itsi 0.2.26 → 0.2.27

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/Cargo.lock +7 -3
  4. data/README.md +4 -1
  5. data/Rakefile +24 -3
  6. data/crates/itsi_acme/Cargo.toml +2 -1
  7. data/crates/itsi_acme/src/acceptor.rs +1 -1
  8. data/crates/itsi_acme/src/acme.rs +31 -3
  9. data/crates/itsi_acme/src/http_challenge.rs +81 -0
  10. data/crates/itsi_acme/src/https_helper.rs +3 -1
  11. data/crates/itsi_acme/src/jose.rs +6 -2
  12. data/crates/itsi_acme/src/lib.rs +2 -0
  13. data/crates/itsi_acme/src/resolver.rs +27 -4
  14. data/crates/itsi_acme/src/state.rs +183 -22
  15. data/crates/itsi_scheduler/Cargo.toml +1 -1
  16. data/crates/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  17. data/crates/itsi_scheduler/src/lib.rs +2 -1
  18. data/crates/itsi_server/Cargo.toml +2 -1
  19. data/crates/itsi_server/src/lib.rs +15 -0
  20. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +0 -1
  21. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  22. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +114 -4
  23. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  24. data/crates/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  25. data/crates/itsi_server/src/server/binds/listener.rs +9 -24
  26. data/crates/itsi_server/src/server/binds/tls.rs +372 -67
  27. data/crates/itsi_server/src/server/signal.rs +14 -9
  28. data/crates/itsi_server/src/services/itsi_http_service.rs +46 -2
  29. data/docs/content/features/_index.md +3 -0
  30. data/docs/content/getting_started/_index.md +6 -2
  31. data/docs/content/getting_started/local_development.md +13 -0
  32. data/docs/content/itsi_scheduler/_index.md +3 -0
  33. data/gems/scheduler/Cargo.lock +4011 -527
  34. data/gems/scheduler/Gemfile +8 -2
  35. data/gems/scheduler/Gemfile.lock +107 -0
  36. data/gems/scheduler/Rakefile +33 -9
  37. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  38. data/gems/scheduler/lib/itsi/scheduler.rb +121 -6
  39. data/gems/scheduler/test/helpers/test_helper.rb +2 -0
  40. data/gems/scheduler/test/test_address_resolve.rb +8 -2
  41. data/gems/scheduler/test/test_itsi_scheduler.rb +80 -0
  42. data/gems/scheduler/test/test_timeout_after.rb +102 -0
  43. data/gems/server/Cargo.lock +30 -1
  44. data/gems/server/Gemfile +3 -0
  45. data/gems/server/Gemfile.lock +195 -0
  46. data/gems/server/Rakefile +18 -5
  47. data/gems/server/lib/itsi/http_request.rb +10 -0
  48. data/gems/server/lib/itsi/server/config/options/certificates.md +37 -9
  49. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.md +2 -0
  50. data/gems/server/lib/itsi/server/default_config/Itsi.rb +4 -0
  51. data/gems/server/lib/itsi/server/rack_interface.rb +47 -3
  52. data/gems/server/lib/itsi/server/version.rb +1 -1
  53. data/gems/server/lib/itsi/server.rb +24 -0
  54. data/gems/server/test/acme/local_acme_challenges.rb +190 -0
  55. data/gems/server/test/helpers/local_acme.rb +218 -0
  56. data/gems/server/test/helpers/test_helper.rb +7 -9
  57. data/gems/server/test/middleware/endpoint.rb +9 -6
  58. data/gems/server/test/rack/test_rack_server.rb +144 -0
  59. data/lib/itsi/version.rb +1 -1
  60. metadata +12 -6
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "rbconfig"
8
+ require "shellwords"
9
+ require "timeout"
10
+ require "tmpdir"
11
+
12
+ class LocalAcmeAuthority
13
+ PEBBLE_VERSION = "v2.10.1"
14
+
15
+ attr_reader :directory_url, :cache_dir, :http_port, :tls_port
16
+
17
+ def self.available?
18
+ system("go version > /dev/null 2>&1")
19
+ end
20
+
21
+ def initialize
22
+ @workspace = Dir.mktmpdir("itsi-local-acme-")
23
+ @bin_dir = File.join(@workspace, "bin")
24
+ @cache_dir = File.join(@workspace, "cache")
25
+ FileUtils.mkdir_p(@bin_dir)
26
+ FileUtils.mkdir_p(@cache_dir)
27
+ @pids = []
28
+ end
29
+
30
+ def start
31
+ install_binaries
32
+ resolve_module_paths
33
+
34
+ @acme_port = free_port
35
+ @management_port = free_port
36
+ @dns_port = free_port
37
+ @dns_management_port = free_port
38
+ @http_port = free_port
39
+ @tls_port = free_port
40
+
41
+ write_pebble_config
42
+ start_challtestsrv
43
+ start_pebble
44
+
45
+ @directory_url = "https://localhost:#{@acme_port}/dir"
46
+ wait_for_https(@directory_url, @pebble_api_ca_path)
47
+ end
48
+
49
+ def stop
50
+ @pids.reverse_each do |pid|
51
+ begin
52
+ Process.kill("TERM", pid)
53
+ rescue Errno::ESRCH
54
+ next
55
+ end
56
+
57
+ begin
58
+ Timeout.timeout(5) { Process.wait(pid) }
59
+ rescue Timeout::Error
60
+ begin
61
+ Process.kill("KILL", pid)
62
+ rescue Errno::ESRCH
63
+ nil
64
+ end
65
+ begin
66
+ Process.wait(pid)
67
+ rescue Errno::ECHILD
68
+ nil
69
+ end
70
+ rescue Errno::ECHILD
71
+ nil
72
+ end
73
+ end
74
+ FileUtils.remove_entry(@workspace) if File.exist?(@workspace)
75
+ end
76
+
77
+ def env
78
+ {
79
+ "ITSI_ACME_DIRECTORY_URL" => @directory_url,
80
+ "ITSI_ACME_CA_PEM_PATH" => @pebble_api_ca_path,
81
+ "ITSI_ACME_CACHE_DIR" => @cache_dir
82
+ }
83
+ end
84
+
85
+ private
86
+
87
+ def install_binaries
88
+ install_go_binary("github.com/letsencrypt/pebble/v2/cmd/pebble", "pebble")
89
+ install_go_binary("github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv", "pebble-challtestsrv")
90
+ end
91
+
92
+ def install_go_binary(package_name, binary_name)
93
+ destination = File.join(@bin_dir, binary_name)
94
+ return if File.exist?(destination)
95
+
96
+ system(
97
+ {
98
+ "GOBIN" => @bin_dir
99
+ },
100
+ "go", "install", "#{package_name}@#{PEBBLE_VERSION}",
101
+ exception: true
102
+ )
103
+ end
104
+
105
+ def resolve_module_paths
106
+ module_json = JSON.parse(`go mod download -json github.com/letsencrypt/pebble/v2@#{PEBBLE_VERSION}`)
107
+ @pebble_module_dir = module_json.fetch("Dir")
108
+ @pebble_api_ca_path = File.join(@pebble_module_dir, "test/certs/pebble.minica.pem")
109
+ @pebble_tls_cert_path = File.join(@pebble_module_dir, "test/certs/localhost/cert.pem")
110
+ @pebble_tls_key_path = File.join(@pebble_module_dir, "test/certs/localhost/key.pem")
111
+ end
112
+
113
+ def write_pebble_config
114
+ @pebble_config_path = File.join(@workspace, "pebble-config.json")
115
+ File.write(
116
+ @pebble_config_path,
117
+ JSON.pretty_generate(
118
+ {
119
+ pebble: {
120
+ listenAddress: "0.0.0.0:#{@acme_port}",
121
+ managementListenAddress: "0.0.0.0:#{@management_port}",
122
+ certificate: @pebble_tls_cert_path,
123
+ privateKey: @pebble_tls_key_path,
124
+ httpPort: @http_port,
125
+ tlsPort: @tls_port,
126
+ ocspResponderURL: "",
127
+ externalAccountBindingRequired: false,
128
+ domainBlocklist: [],
129
+ retryAfter: {
130
+ authz: 1,
131
+ order: 1
132
+ },
133
+ keyAlgorithm: "ecdsa"
134
+ }
135
+ }
136
+ )
137
+ )
138
+ end
139
+
140
+ def start_challtestsrv
141
+ spawn_process(
142
+ [
143
+ File.join(@bin_dir, "pebble-challtestsrv"),
144
+ "-dnsserver", "127.0.0.1:#{@dns_port}",
145
+ "-management", "127.0.0.1:#{@dns_management_port}",
146
+ "-http01", "",
147
+ "-https01", "",
148
+ "-tlsalpn01", "",
149
+ "-defaultIPv4", "127.0.0.1",
150
+ "-defaultIPv6", ""
151
+ ],
152
+ "challtestsrv"
153
+ )
154
+ end
155
+
156
+ def start_pebble
157
+ spawn_process(
158
+ [
159
+ File.join(@bin_dir, "pebble"),
160
+ "-config", @pebble_config_path,
161
+ "-dnsserver", "127.0.0.1:#{@dns_port}",
162
+ "-strict=false"
163
+ ],
164
+ "pebble",
165
+ {
166
+ "PEBBLE_VA_NOSLEEP" => "1",
167
+ "PEBBLE_WFE_NONCEREJECT" => "0",
168
+ "PEBBLE_AUTHZREUSE" => "0"
169
+ }
170
+ )
171
+ end
172
+
173
+ def spawn_process(command, name, env = {})
174
+ log_path = File.join(@workspace, "#{name}.log")
175
+ log = File.open(log_path, "w")
176
+ pid = Process.spawn(
177
+ env,
178
+ *command,
179
+ out: log,
180
+ err: log
181
+ )
182
+ @pids << pid
183
+ end
184
+
185
+ def free_port
186
+ server = TCPServer.new("127.0.0.1", 0)
187
+ port = server.addr[1]
188
+ server.close
189
+ port
190
+ end
191
+
192
+ def wait_for_https(url, ca_file)
193
+ uri = URI(url)
194
+ store = OpenSSL::X509::Store.new
195
+ store.add_file(ca_file)
196
+
197
+ Timeout.timeout(20) do
198
+ loop do
199
+ begin
200
+ Net::HTTP.start(
201
+ uri.host,
202
+ uri.port,
203
+ use_ssl: true,
204
+ cert_store: store,
205
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
206
+ open_timeout: 1,
207
+ read_timeout: 1
208
+ ) do |http|
209
+ response = http.get(uri.request_uri)
210
+ return if response.code.to_i < 500
211
+ end
212
+ rescue StandardError
213
+ sleep 0.1
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  ENV["ITSI_LOG"] = "off"
4
4
 
5
+ Warning[:experimental] = false if Warning.respond_to?(:[]=)
6
+
5
7
  require "minitest/reporters"
6
8
  require "rackup"
7
9
  require "itsi/server"
@@ -26,12 +28,12 @@ def free_bind(protocol = "http", unix_socket: false)
26
28
  end
27
29
 
28
30
  def server(
29
- app: nil, app_with_lint: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, cleanup: true,
31
+ app: nil, app_with_lint: nil, protocol: "http", bind: free_bind(protocol), binds: nil, itsi_rb: nil, cleanup: true,
30
32
  &blk)
31
33
  app ||= Rack::Lint.new(app_with_lint) if app_with_lint
32
34
 
33
35
  cli_params = {}
34
- cli_params[:binds] = [bind] if bind
36
+ cli_params[:binds] = binds || [bind] if binds || bind
35
37
 
36
38
  sync = Queue.new
37
39
  cli_params[:hooks] ||= {}
@@ -40,10 +42,10 @@ def server(
40
42
  end
41
43
 
42
44
  Itsi::Server.start_in_background_thread(cli_params) do
43
- bind bind
45
+ bind bind if bind && binds.nil?
44
46
  workers 1
45
47
  threads 1
46
- log_level :warn
48
+ log_level :error
47
49
  run app if app
48
50
  instance_exec(&itsi_rb) if itsi_rb
49
51
  end
@@ -51,11 +53,6 @@ def server(
51
53
  sync.pop
52
54
  uri = URI(bind)
53
55
  RequestContext.new(uri, self).instance_exec(uri, &blk)
54
- rescue StandardError => e
55
- puts e
56
- # puts e.message
57
- # puts e.backtrace.join("\n")
58
- raise
59
56
  ensure
60
57
  Itsi::Server.stop_background_threads if cleanup
61
58
  end
@@ -157,6 +154,7 @@ class RequestContext
157
154
  @uri.host,
158
155
  @uri.port,
159
156
  use_ssl: @uri.scheme == "https",
157
+ verify_mode: (@uri.scheme == "https" ? OpenSSL::SSL::VERIFY_NONE : nil),
160
158
  **opts
161
159
  )
162
160
  end
@@ -118,14 +118,17 @@ class TestEndpoint < Minitest::Test
118
118
 
119
119
  # 8. Exceptions inside handler produce 500
120
120
  def test_internal_error_returns_500
121
- server(itsi_rb: lambda do
122
- endpoint "/boom" do |_req|
123
- raise "test crash"
121
+ response = nil
122
+ capture_subprocess_io do
123
+ server(itsi_rb: lambda do
124
+ endpoint "/boom" do |_req|
125
+ raise "test crash"
126
+ end
127
+ end) do
128
+ response = get_resp("/boom")
124
129
  end
125
- end) do
126
- res = get_resp("/boom")
127
- assert_equal "500", res.code
128
130
  end
131
+ assert_equal "500", response.code
129
132
  end
130
133
 
131
134
  # 9. Controller method dispatch via symbol
@@ -1,4 +1,7 @@
1
1
  require_relative "../helpers/test_helper"
2
+ require "async/websocket/adapters/rack"
3
+ require "protocol/websocket/connection"
4
+ require "base64"
2
5
 
3
6
  class TestRackServer < Minitest::Test
4
7
  def test_that_it_has_a_version_number
@@ -66,6 +69,115 @@ class TestRackServer < Minitest::Test
66
69
  end
67
70
  end
68
71
 
72
+ def test_partial_hijack_yields_writable_stream
73
+ stream_class = nil
74
+ stream_closed = nil
75
+ required_methods_ok = nil
76
+
77
+ server(app_with_lint: lambda do |_env|
78
+ [200, { "content-type" => "text/plain", "rack.hijack" => lambda { |stream|
79
+ stream_class = stream.class
80
+ refute stream.closed?
81
+ required_methods_ok = %i[read write << flush close close_read close_write closed?]
82
+ .all? { |method_name| stream.respond_to?(method_name) }
83
+ stream.write("Hello")
84
+ stream.write(", World!")
85
+ stream.close
86
+ stream_closed = stream.closed?
87
+ } }, []]
88
+ end) do
89
+ assert_equal "Hello, World!", get("/")
90
+ end
91
+
92
+ assert_equal Rack::Lint::Wrapper::StreamWrapper, stream_class
93
+ assert_equal true, required_methods_ok
94
+ assert_equal true, stream_closed
95
+ end
96
+
97
+ def test_partial_hijack_upgrade_supports_bidirectional_io
98
+ callback_error = Queue.new
99
+ stream_details = Queue.new
100
+
101
+ server(app: lambda do |_env|
102
+ [101, {
103
+ "connection" => "Upgrade",
104
+ "upgrade" => "test",
105
+ "rack.hijack" => lambda { |stream|
106
+ stream_details << [stream.class, stream.is_a?(IO), stream.method(:read).arity]
107
+ Thread.new do
108
+ begin
109
+ payload = stream.read(2)
110
+ stream.write("echo:#{payload}")
111
+ rescue => e
112
+ callback_error << e
113
+ ensure
114
+ stream.close rescue nil
115
+ end
116
+ end
117
+ }
118
+ }, []]
119
+ end) do |uri|
120
+ socket = TCPSocket.new(uri.host, uri.port)
121
+ socket.write(
122
+ "GET / HTTP/1.1\r\n" \
123
+ "Host: #{uri.host}:#{uri.port}\r\n" \
124
+ "Connection: Upgrade\r\n" \
125
+ "Upgrade: test\r\n" \
126
+ "\r\n"
127
+ )
128
+ socket.flush
129
+
130
+ response = +""
131
+ until response.include?("\r\n\r\n")
132
+ response << socket.readpartial(1024)
133
+ end
134
+
135
+ assert_includes response, "101"
136
+ socket.write("hi")
137
+ socket.flush
138
+ echoed = socket.readpartial(7)
139
+ assert_equal "echo:hi", echoed
140
+ ensure
141
+ socket&.close
142
+ end
143
+
144
+ raise callback_error.pop unless callback_error.empty?
145
+ stream_class, stream_is_io, read_arity = stream_details.pop
146
+ assert_equal UNIXSocket, stream_class
147
+ assert_equal true, stream_is_io
148
+ assert_operator read_arity, :!=, 0
149
+ end
150
+
151
+ def test_async_websocket_rack_adapter_can_upgrade_and_echo_messages
152
+ callback_error = Queue.new
153
+
154
+ server(app_with_lint: lambda do |env|
155
+ Async::WebSocket::Adapters::Rack.open(env) do |connection|
156
+ begin
157
+ message = connection.read
158
+ connection.write(message)
159
+ connection.flush
160
+ rescue => e
161
+ callback_error << e
162
+ end
163
+ end || [200, { "content-type" => "text/plain" }, ["ok"]]
164
+ end) do |uri|
165
+ socket = websocket_upgrade_socket(uri)
166
+ connection = websocket_client_connection(socket)
167
+
168
+ connection.write("hello")
169
+ connection.flush
170
+
171
+ message = connection.read
172
+ assert_equal "hello", message.to_str
173
+ ensure
174
+ connection&.close rescue nil
175
+ socket&.close
176
+ end
177
+
178
+ raise callback_error.pop unless callback_error.empty?
179
+ end
180
+
69
181
  def test_enumerable_body
70
182
  server(app_with_lint: lambda do |env|
71
183
  [200, { "content-type" => "application/json" },
@@ -424,4 +536,36 @@ class TestRackServer < Minitest::Test
424
536
  assert_equal %w[a=b c=d], resp.get_fields("set-cookie")
425
537
  end
426
538
  end
539
+
540
+ private
541
+
542
+ def websocket_upgrade_socket(uri)
543
+ socket = TCPSocket.new(uri.host, uri.port)
544
+ socket.write(
545
+ "GET / HTTP/1.1\r\n" \
546
+ "Host: #{uri.host}:#{uri.port}\r\n" \
547
+ "Connection: Upgrade\r\n" \
548
+ "Upgrade: websocket\r\n" \
549
+ "Sec-WebSocket-Version: 13\r\n" \
550
+ "Sec-WebSocket-Key: #{Base64.strict_encode64(SecureRandom.random_bytes(16))}\r\n" \
551
+ "\r\n"
552
+ )
553
+ socket.flush
554
+
555
+ response = +""
556
+ until response.include?("\r\n\r\n")
557
+ chunk = socket.read(1)
558
+ raise EOFError, "Unexpected EOF during websocket handshake" unless chunk
559
+
560
+ response << chunk
561
+ end
562
+
563
+ assert_includes response, "101"
564
+ socket
565
+ end
566
+
567
+ def websocket_client_connection(socket)
568
+ framer = Protocol::WebSocket::Framer.new(socket)
569
+ Protocol::WebSocket::Connection.new(framer, mask: "\x01\x02\x03\x04")
570
+ end
427
571
  end
data/lib/itsi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = "0.2.26"
2
+ VERSION = "0.2.27"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.26
4
+ version: 0.2.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.26
18
+ version: 0.2.27
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.2.26
25
+ version: 0.2.27
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: itsi-server
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.2.26
32
+ version: 0.2.27
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.2.26
39
+ version: 0.2.27
40
40
  description: Wrapper Gem for both the Itsi server and the Itsi Fiber scheduler
41
41
  email:
42
42
  - wc@pico.net.nz
@@ -74,6 +74,7 @@ files:
74
74
  - crates/itsi_acme/src/caches/no.rs
75
75
  - crates/itsi_acme/src/caches/test.rs
76
76
  - crates/itsi_acme/src/config.rs
77
+ - crates/itsi_acme/src/http_challenge.rs
77
78
  - crates/itsi_acme/src/https_helper.rs
78
79
  - crates/itsi_acme/src/incoming.rs
79
80
  - crates/itsi_acme/src/jose.rs
@@ -768,6 +769,7 @@ files:
768
769
  - gems/scheduler/Cargo.lock
769
770
  - gems/scheduler/Cargo.toml
770
771
  - gems/scheduler/Gemfile
772
+ - gems/scheduler/Gemfile.lock
771
773
  - gems/scheduler/Rakefile
772
774
  - gems/scheduler/bin/console
773
775
  - gems/scheduler/bin/setup
@@ -787,11 +789,13 @@ files:
787
789
  - gems/scheduler/test/test_nested_fibers.rb
788
790
  - gems/scheduler/test/test_network_io.rb
789
791
  - gems/scheduler/test/test_process_wait.rb
792
+ - gems/scheduler/test/test_timeout_after.rb
790
793
  - gems/server/.gitignore
791
794
  - gems/server/.rubocop.yml
792
795
  - gems/server/Cargo.lock
793
796
  - gems/server/Cargo.toml
794
797
  - gems/server/Gemfile
798
+ - gems/server/Gemfile.lock
795
799
  - gems/server/Rakefile
796
800
  - gems/server/bin/console
797
801
  - gems/server/bin/setup
@@ -1084,6 +1088,8 @@ files:
1084
1088
  - gems/server/lib/itsi/standard_headers.rb
1085
1089
  - gems/server/lib/ruby_lsp/itsi/addon.rb
1086
1090
  - gems/server/lib/shell_completions/completions.rb
1091
+ - gems/server/test/acme/local_acme_challenges.rb
1092
+ - gems/server/test/helpers/local_acme.rb
1087
1093
  - gems/server/test/helpers/test_helper.rb
1088
1094
  - gems/server/test/middleware/allow_list.rb
1089
1095
  - gems/server/test/middleware/auth_api_key.rb
@@ -1155,7 +1161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1155
1161
  - !ruby/object:Gem::Version
1156
1162
  version: '3.2'
1157
1163
  requirements: []
1158
- rubygems_version: 4.0.3
1164
+ rubygems_version: 3.6.9
1159
1165
  specification_version: 4
1160
1166
  summary: Wrapper Gem for both the Itsi server and the Itsi Fiber scheduler
1161
1167
  test_files: []