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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Cargo.lock +7 -3
- data/README.md +4 -1
- data/Rakefile +24 -3
- data/crates/itsi_acme/Cargo.toml +2 -1
- data/crates/itsi_acme/src/acceptor.rs +1 -1
- data/crates/itsi_acme/src/acme.rs +31 -3
- data/crates/itsi_acme/src/http_challenge.rs +81 -0
- data/crates/itsi_acme/src/https_helper.rs +3 -1
- data/crates/itsi_acme/src/jose.rs +6 -2
- data/crates/itsi_acme/src/lib.rs +2 -0
- data/crates/itsi_acme/src/resolver.rs +27 -4
- data/crates/itsi_acme/src/state.rs +183 -22
- data/crates/itsi_scheduler/Cargo.toml +1 -1
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +115 -64
- data/crates/itsi_scheduler/src/lib.rs +2 -1
- data/crates/itsi_server/Cargo.toml +2 -1
- data/crates/itsi_server/src/lib.rs +15 -0
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +0 -1
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +114 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +100 -0
- data/crates/itsi_server/src/server/binds/listener.rs +9 -24
- data/crates/itsi_server/src/server/binds/tls.rs +372 -67
- data/crates/itsi_server/src/server/signal.rs +14 -9
- data/crates/itsi_server/src/services/itsi_http_service.rs +46 -2
- data/docs/content/features/_index.md +3 -0
- data/docs/content/getting_started/_index.md +6 -2
- data/docs/content/getting_started/local_development.md +13 -0
- data/docs/content/itsi_scheduler/_index.md +3 -0
- data/gems/scheduler/Cargo.lock +4011 -527
- data/gems/scheduler/Gemfile +8 -2
- data/gems/scheduler/Gemfile.lock +107 -0
- data/gems/scheduler/Rakefile +33 -9
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/lib/itsi/scheduler.rb +121 -6
- data/gems/scheduler/test/helpers/test_helper.rb +2 -0
- data/gems/scheduler/test/test_address_resolve.rb +8 -2
- data/gems/scheduler/test/test_itsi_scheduler.rb +80 -0
- data/gems/scheduler/test/test_timeout_after.rb +102 -0
- data/gems/server/Cargo.lock +30 -1
- data/gems/server/Gemfile +3 -0
- data/gems/server/Gemfile.lock +195 -0
- data/gems/server/Rakefile +18 -5
- data/gems/server/lib/itsi/http_request.rb +10 -0
- data/gems/server/lib/itsi/server/config/options/certificates.md +37 -9
- data/gems/server/lib/itsi/server/config/options/fiber_scheduler.md +2 -0
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +4 -0
- data/gems/server/lib/itsi/server/rack_interface.rb +47 -3
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +24 -0
- data/gems/server/test/acme/local_acme_challenges.rb +190 -0
- data/gems/server/test/helpers/local_acme.rb +218 -0
- data/gems/server/test/helpers/test_helper.rb +7 -9
- data/gems/server/test/middleware/endpoint.rb +9 -6
- data/gems/server/test/rack/test_rack_server.rb +144 -0
- data/lib/itsi/version.rb +1 -1
- metadata +12 -6
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: ../scheduler
|
|
3
|
+
specs:
|
|
4
|
+
itsi-scheduler (0.2.27)
|
|
5
|
+
rb_sys (~> 0.9.91)
|
|
6
|
+
|
|
7
|
+
PATH
|
|
8
|
+
remote: .
|
|
9
|
+
specs:
|
|
10
|
+
itsi-server (0.2.27)
|
|
11
|
+
json (~> 2)
|
|
12
|
+
prism (~> 1.4)
|
|
13
|
+
rack (>= 1.6)
|
|
14
|
+
rb_sys (~> 0.9.91)
|
|
15
|
+
|
|
16
|
+
GEM
|
|
17
|
+
remote: https://rubygems.org/
|
|
18
|
+
specs:
|
|
19
|
+
ansi (1.6.0)
|
|
20
|
+
async (2.39.0)
|
|
21
|
+
console (~> 1.29)
|
|
22
|
+
fiber-annotation
|
|
23
|
+
io-event (~> 1.11)
|
|
24
|
+
metrics (~> 0.12)
|
|
25
|
+
traces (~> 0.18)
|
|
26
|
+
async-http (0.95.1)
|
|
27
|
+
async (>= 2.10.2)
|
|
28
|
+
async-pool (~> 0.11)
|
|
29
|
+
io-endpoint (~> 0.14)
|
|
30
|
+
io-stream (~> 0.6)
|
|
31
|
+
metrics (~> 0.12)
|
|
32
|
+
protocol-http (~> 0.62)
|
|
33
|
+
protocol-http1 (~> 0.39)
|
|
34
|
+
protocol-http2 (~> 0.26)
|
|
35
|
+
protocol-url (~> 0.2)
|
|
36
|
+
traces (~> 0.10)
|
|
37
|
+
async-pool (0.11.2)
|
|
38
|
+
async (>= 2.0)
|
|
39
|
+
async-websocket (0.30.1)
|
|
40
|
+
async-http (~> 0.76)
|
|
41
|
+
protocol-http (~> 0.34)
|
|
42
|
+
protocol-rack (~> 0.7)
|
|
43
|
+
protocol-websocket (~> 0.17)
|
|
44
|
+
base64 (0.3.0)
|
|
45
|
+
bigdecimal (4.1.2)
|
|
46
|
+
builder (3.3.0)
|
|
47
|
+
connection_pool (3.0.2)
|
|
48
|
+
console (1.36.0)
|
|
49
|
+
fiber-annotation
|
|
50
|
+
fiber-local (~> 1.1)
|
|
51
|
+
json
|
|
52
|
+
fiber-annotation (0.2.0)
|
|
53
|
+
fiber-local (1.1.0)
|
|
54
|
+
fiber-storage
|
|
55
|
+
fiber-storage (1.0.1)
|
|
56
|
+
google-protobuf (4.30.2)
|
|
57
|
+
bigdecimal
|
|
58
|
+
rake (>= 13)
|
|
59
|
+
googleapis-common-protos-types (1.19.0)
|
|
60
|
+
google-protobuf (>= 3.18, < 5.a)
|
|
61
|
+
grpc (1.78.0)
|
|
62
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
63
|
+
googleapis-common-protos-types (~> 1.0)
|
|
64
|
+
io-endpoint (0.17.2)
|
|
65
|
+
io-event (1.16.2)
|
|
66
|
+
io-stream (0.13.1)
|
|
67
|
+
json (2.19.4)
|
|
68
|
+
jwt (3.1.2)
|
|
69
|
+
base64
|
|
70
|
+
language_server-protocol (3.17.0.5)
|
|
71
|
+
logger (1.7.0)
|
|
72
|
+
metrics (0.15.0)
|
|
73
|
+
minitest (5.27.0)
|
|
74
|
+
minitest-reporters (1.8.0)
|
|
75
|
+
ansi
|
|
76
|
+
builder
|
|
77
|
+
minitest (>= 5.0, < 7)
|
|
78
|
+
ruby-progressbar
|
|
79
|
+
net_http_unix (0.2.2)
|
|
80
|
+
prism (1.9.0)
|
|
81
|
+
protocol-hpack (1.5.1)
|
|
82
|
+
protocol-http (0.62.2)
|
|
83
|
+
protocol-http1 (0.39.0)
|
|
84
|
+
protocol-http (~> 0.62)
|
|
85
|
+
protocol-http2 (0.26.0)
|
|
86
|
+
protocol-hpack (~> 1.4)
|
|
87
|
+
protocol-http (~> 0.62)
|
|
88
|
+
protocol-rack (0.22.1)
|
|
89
|
+
io-stream (>= 0.10)
|
|
90
|
+
protocol-http (~> 0.58)
|
|
91
|
+
rack (>= 1.0)
|
|
92
|
+
protocol-url (0.4.0)
|
|
93
|
+
protocol-websocket (0.21.1)
|
|
94
|
+
protocol-http (~> 0.2)
|
|
95
|
+
rack (3.2.6)
|
|
96
|
+
rackup (2.3.1)
|
|
97
|
+
rack (>= 3)
|
|
98
|
+
rake (13.3.1)
|
|
99
|
+
rake-compiler (1.3.0)
|
|
100
|
+
rake
|
|
101
|
+
rake-compiler-dock (1.11.0)
|
|
102
|
+
rb_sys (0.9.126)
|
|
103
|
+
json (>= 2)
|
|
104
|
+
rake-compiler-dock (= 1.11.0)
|
|
105
|
+
rbs (3.10.3)
|
|
106
|
+
logger
|
|
107
|
+
tsort
|
|
108
|
+
redis (5.4.1)
|
|
109
|
+
redis-client (>= 0.22.0)
|
|
110
|
+
redis-client (0.24.0)
|
|
111
|
+
connection_pool
|
|
112
|
+
ruby-lsp (0.26.7)
|
|
113
|
+
language_server-protocol (~> 3.17.0)
|
|
114
|
+
prism (>= 1.2, < 2.0)
|
|
115
|
+
rbs (>= 3, < 5)
|
|
116
|
+
ruby-progressbar (1.13.0)
|
|
117
|
+
traces (0.18.2)
|
|
118
|
+
tsort (0.2.0)
|
|
119
|
+
|
|
120
|
+
PLATFORMS
|
|
121
|
+
arm64-darwin-24
|
|
122
|
+
ruby
|
|
123
|
+
|
|
124
|
+
DEPENDENCIES
|
|
125
|
+
async-websocket
|
|
126
|
+
bundler
|
|
127
|
+
grpc
|
|
128
|
+
itsi-scheduler!
|
|
129
|
+
itsi-server!
|
|
130
|
+
jwt
|
|
131
|
+
minitest (~> 5.16)
|
|
132
|
+
minitest-reporters
|
|
133
|
+
net_http_unix
|
|
134
|
+
rack
|
|
135
|
+
rackup
|
|
136
|
+
rake (~> 13.0)
|
|
137
|
+
rake-compiler
|
|
138
|
+
rb_sys (~> 0.9.91)
|
|
139
|
+
redis
|
|
140
|
+
ruby-lsp
|
|
141
|
+
|
|
142
|
+
CHECKSUMS
|
|
143
|
+
ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
|
|
144
|
+
async (2.39.0) sha256=df18730073f2bbb45788077dfa20cb365ecc1b9453969f44de6796b5191a00aa
|
|
145
|
+
async-http (0.95.1) sha256=0c3dd458c204c06d5c4b20b01bbec4794a1203db627fb2ce536e1799ec14786c
|
|
146
|
+
async-pool (0.11.2) sha256=0a43a17b02b04d9c451b7d12fafa9a50e55dc6dd00d4369aca00433f16a7e3ed
|
|
147
|
+
async-websocket (0.30.1) sha256=54bb8a8f184e4aa64434c7a78ecc55850a67a3d1dcd02e3ae787376e2c673936
|
|
148
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
149
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
150
|
+
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
|
151
|
+
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
152
|
+
console (1.36.0) sha256=45599ea906cf80a73d8941f03abf873fe66a6a954e0bac5bc1c01e2cdc406f07
|
|
153
|
+
fiber-annotation (0.2.0) sha256=7abfadf1d119f508867d4103bf231c0354d019cc39a5738945dec2edadaf6c03
|
|
154
|
+
fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06
|
|
155
|
+
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
|
156
|
+
google-protobuf (4.30.2) sha256=0f35168dbeeccf13d928acf6c128cfec17b9a826ae4505246a02c115f4ae16ed
|
|
157
|
+
googleapis-common-protos-types (1.19.0) sha256=aecb76ca5326f8bcc47ab083259bbc4971d07e87f56808af7e210669d9765694
|
|
158
|
+
grpc (1.78.0) sha256=7aaf47b6d7783fb0e7d40fc853d34e802d47aef7b1312862b6e719141b3527c9
|
|
159
|
+
io-endpoint (0.17.2) sha256=3feaf766c116b35839c11fac68b6aaadc47887bb488902a57bf8e1d288fb3338
|
|
160
|
+
io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
|
|
161
|
+
io-stream (0.13.1) sha256=570d7c4dfb0fbd767480b4a222048a2be6d9b78febc1ec68258d0e0a4cde20de
|
|
162
|
+
itsi-scheduler (0.2.27)
|
|
163
|
+
itsi-server (0.2.27)
|
|
164
|
+
json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
|
|
165
|
+
jwt (3.1.2) sha256=af6991f19a6bb4060d618d9add7a66f0eeb005ac0bc017cd01f63b42e122d535
|
|
166
|
+
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
167
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
168
|
+
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
|
169
|
+
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
|
|
170
|
+
minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
|
|
171
|
+
net_http_unix (0.2.2) sha256=98aa0e1e7787f8383e8dd8ff60fc18062c2ddb2aadbc76e92cfb4f95d2c9d71d
|
|
172
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
173
|
+
protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91
|
|
174
|
+
protocol-http (0.62.2) sha256=e1c1f2f56029c1af8c4e2b8a67d0d096c76620f3afd8d99d1dcd2f6b8ffa773b
|
|
175
|
+
protocol-http1 (0.39.0) sha256=e49b3f4cda6f5d94c76a323d2b7f6977cba3ebd082d2da437039594da77ad8eb
|
|
176
|
+
protocol-http2 (0.26.0) sha256=bac89cd78082b241ccd0cf7246f5160e4bb0c9c975fb4bf7deef5f88cc317486
|
|
177
|
+
protocol-rack (0.22.1) sha256=1185d245927ef9849a603700d6991ca353bc89724fbf98efa4a4333ed62a9fc3
|
|
178
|
+
protocol-url (0.4.0) sha256=64d4c03b6b51ad815ac6fdaf77a1d91e5baf9220d26becb846c5459dacdea9e1
|
|
179
|
+
protocol-websocket (0.21.1) sha256=34325e4325697f0956877e67784bcc838cfd51ebbf4f8e9e5201be292041ee61
|
|
180
|
+
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
|
|
181
|
+
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
|
182
|
+
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
183
|
+
rake-compiler (1.3.0) sha256=eec272ef6d4dad27b36f5cdcf5b9ee4df2193751f4082b095f981ebf9cdf4127
|
|
184
|
+
rake-compiler-dock (1.11.0) sha256=eab51f2cd533eb35cea6b624a75281f047123e70a64c58b607471bb49428f8c2
|
|
185
|
+
rb_sys (0.9.126) sha256=ba958e0b8b4b89eeae0b3d24b64c809eb2c37e0ab0773a49e9b1c2e22c95aef8
|
|
186
|
+
rbs (3.10.3) sha256=70627f3919016134d554e6c99195552ae3ef6020fe034c8e983facc9c192daa6
|
|
187
|
+
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
|
188
|
+
redis-client (0.24.0) sha256=ee65ee39cb2c38608b734566167fd912384f3c1241f59075e22858f23a085dbb
|
|
189
|
+
ruby-lsp (0.26.7) sha256=60a1199fc7e329348d63a2479854f94435725d833eeabf3d539b790185cbf21f
|
|
190
|
+
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
191
|
+
traces (0.18.2) sha256=80f1649cb4daace1d7174b81f3b3b7427af0b93047759ba349960cb8f315e214
|
|
192
|
+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
193
|
+
|
|
194
|
+
BUNDLED WITH
|
|
195
|
+
2.6.9
|
data/gems/server/Rakefile
CHANGED
|
@@ -29,6 +29,16 @@ SMOKE_TEST_GLOBS = %w[
|
|
|
29
29
|
test/middleware/string_rewrite.rb
|
|
30
30
|
].freeze
|
|
31
31
|
|
|
32
|
+
def native_build_tasks_requested?
|
|
33
|
+
requested = Rake.application.top_level_tasks
|
|
34
|
+
return true if requested.empty?
|
|
35
|
+
|
|
36
|
+
requested.any? do |task_name|
|
|
37
|
+
task_name == "default" ||
|
|
38
|
+
task_name.start_with?("build", "compile", "cross", "clobber")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
def configure_test_task(task_name, test_globs)
|
|
33
43
|
Minitest::TestTask.create(task_name) do |t|
|
|
34
44
|
t.libs << "test"
|
|
@@ -41,17 +51,20 @@ end
|
|
|
41
51
|
|
|
42
52
|
configure_test_task(:test, ["test/**/*.rb"])
|
|
43
53
|
configure_test_task("test:smoke", SMOKE_TEST_GLOBS)
|
|
54
|
+
configure_test_task("test:acme", ["test/acme/**/*.rb"])
|
|
44
55
|
|
|
45
56
|
task "test:full" => :test
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
if native_build_tasks_requested?
|
|
59
|
+
require "rb_sys/extensiontask"
|
|
48
60
|
|
|
49
|
-
task build: :compile
|
|
61
|
+
task build: :compile
|
|
50
62
|
|
|
51
|
-
GEMSPEC = Gem::Specification.load("itsi-server.gemspec")
|
|
63
|
+
GEMSPEC = Gem::Specification.load("itsi-server.gemspec")
|
|
52
64
|
|
|
53
|
-
RbSys::ExtensionTask.new("itsi-server", GEMSPEC) do |ext|
|
|
54
|
-
|
|
65
|
+
RbSys::ExtensionTask.new("itsi-server", GEMSPEC) do |ext|
|
|
66
|
+
ext.lib_dir = "lib/itsi/server"
|
|
67
|
+
end
|
|
55
68
|
end
|
|
56
69
|
|
|
57
70
|
task default: %i[compile test rubocop]
|
|
@@ -145,6 +145,16 @@ module Itsi
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
def partial_hijack
|
|
149
|
+
UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
|
|
150
|
+
server_sock.autoclose = false
|
|
151
|
+
response.partial_hijack(server_sock.fileno)
|
|
152
|
+
server_sock.sync = true
|
|
153
|
+
app_sock.sync = true
|
|
154
|
+
app_sock
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
148
158
|
# Rack expects env["rack.hijack"] to respond to #call.
|
|
149
159
|
def call
|
|
150
160
|
hijack
|
|
@@ -54,12 +54,40 @@ Let's Encrypt enforces strict rate limits on production certificate generation.
|
|
|
54
54
|
{{< /callout >}}
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
Itsi supports both ACME challenge types that matter for common deployments:
|
|
58
|
+
|
|
59
|
+
* `TLS-ALPN-01` is used when the certificate authority can reach Itsi directly on the HTTPS listener.
|
|
60
|
+
* `HTTP-01` can be used when you also expose a reachable HTTP listener for the same hostname. In real Let's Encrypt deployments this typically means port `80` must reach Itsi for `/.well-known/acme-challenge/*`.
|
|
61
|
+
|
|
62
|
+
This means setups behind a CDN, WAF, or TLS-terminating proxy can still use automated certificates, provided plain HTTP validation traffic is forwarded to Itsi.
|
|
63
|
+
|
|
64
|
+
E.g. a production configuration that allows HTTP-01 fallback might look like this:
|
|
65
|
+
|
|
66
|
+
```ruby {filename=Itsi.rb}
|
|
67
|
+
bind "http://0.0.0.0:80"
|
|
68
|
+
bind "https://0.0.0.0:443?cert=acme&domains=example.com&acme_email=you@example.com"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Dynamic Domain Registration
|
|
72
|
+
You can add or remove ACME-managed domains while Itsi is already running.
|
|
73
|
+
|
|
74
|
+
This is useful when hostnames are discovered dynamically by your Ruby application, or when you want to defer certificate issuance until a tenant, customer, or site is activated.
|
|
75
|
+
|
|
76
|
+
Runtime APIs:
|
|
77
|
+
|
|
78
|
+
* `Itsi::Server.tls_bindings`
|
|
79
|
+
* `Itsi::Server.tls_domains(listener_id = nil)`
|
|
80
|
+
* `Itsi::Server.tls_domain_statuses(listener_id = nil)`
|
|
81
|
+
* `Itsi::Server.register_tls_domain(domain, listener_id = nil)`
|
|
82
|
+
* `Itsi::Server.unregister_tls_domain(domain, listener_id = nil)`
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Itsi::Server.register_tls_domain("customer-a.example.com")
|
|
88
|
+
|
|
89
|
+
status = Itsi::Server.tls_domain_statuses.find { |entry| entry["domain"] == "customer-a.example.com" }
|
|
90
|
+
puts status
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When using dynamic issuance with HTTP-01, the same requirement still applies: the domain being issued must be able to reach an Itsi-managed HTTP listener for the ACME challenge path.
|
|
@@ -7,6 +7,8 @@ This allows Itsi to process a very large number of IO heavy requests concurrentl
|
|
|
7
7
|
|
|
8
8
|
Enabling Fiber Scheduler mode can drastically improve application performance if you perform large amounts of blocking IO operations.
|
|
9
9
|
|
|
10
|
+
Itsi's bundled scheduler is intended to be practical for real Ruby applications, not just toy socket examples. It integrates with the scheduler hooks used by modern Rubies for socket I/O, DNS lookups, sleeps and timeouts, and process waiting.
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
## Configuration File
|
|
12
14
|
```ruby {filename="Itsi.rb"}
|
|
@@ -37,6 +37,10 @@ fiber_scheduler nil
|
|
|
37
37
|
# bind "https://itsi.fyi?cert=acme&acme_email=admin@itsi.fyi"
|
|
38
38
|
# You can generate certificates for multiple domains at once, by passing a comma-separated list of domains
|
|
39
39
|
# bind "https://0.0.0.0?domains=foo.itsi.fyi,bar.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
|
|
40
|
+
# If HTTPS on 443 is not directly reachable, you can also expose an HTTP listener and
|
|
41
|
+
# Let's Encrypt will be able to validate using HTTP-01 instead.
|
|
42
|
+
# bind "http://0.0.0.0:80"
|
|
43
|
+
# bind "https://0.0.0.0:443?domains=foo.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
|
|
40
44
|
#
|
|
41
45
|
# If you already have a certificate you can specify it using the cert and key parameters
|
|
42
46
|
# bind "https://itsi.fyi?cert=/path/to/cert.pem&key=/path/to/key.pem"
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
module Itsi
|
|
2
2
|
class Server
|
|
3
3
|
module RackInterface
|
|
4
|
+
class PartialHijackStream
|
|
5
|
+
def initialize(response)
|
|
6
|
+
@response = response
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def write(chunk)
|
|
10
|
+
@response.write(chunk.to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(*)
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def <<(chunk)
|
|
18
|
+
write(chunk)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def flush
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close_write
|
|
27
|
+
@response.close_write
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def close_read
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
@response.close_write
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def closed?
|
|
39
|
+
@response.closed?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
4
43
|
# Builds a handler proc that is compatible with Rack applications.
|
|
5
44
|
def self.for(app)
|
|
6
45
|
require "rack"
|
|
@@ -38,7 +77,8 @@ module Itsi
|
|
|
38
77
|
response.status = status
|
|
39
78
|
|
|
40
79
|
# 2. Set Headers
|
|
41
|
-
|
|
80
|
+
hijack_callback = headers.delete("rack.hijack")
|
|
81
|
+
body_streamer = streaming_body?(body) ? body : hijack_callback
|
|
42
82
|
|
|
43
83
|
response.reserve_headers(headers.size)
|
|
44
84
|
|
|
@@ -57,10 +97,14 @@ module Itsi
|
|
|
57
97
|
# the server will begin to stream it to the client.
|
|
58
98
|
|
|
59
99
|
|
|
60
|
-
if
|
|
100
|
+
if hijack_callback
|
|
101
|
+
stream = status == 101 ? request.partial_hijack : PartialHijackStream.new(response)
|
|
102
|
+
body_streamer.call(stream)
|
|
103
|
+
elsif body_streamer
|
|
61
104
|
# If we're partially hijacked or returned a streaming body,
|
|
62
105
|
# stream this response.
|
|
63
|
-
|
|
106
|
+
stream = status == 101 ? request.partial_hijack : response
|
|
107
|
+
body_streamer.call(stream)
|
|
64
108
|
|
|
65
109
|
elsif body.is_a?(Array)
|
|
66
110
|
if body.length == 1
|
|
@@ -32,6 +32,30 @@ module Itsi
|
|
|
32
32
|
@running && !@running.empty?
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def current_server
|
|
36
|
+
@running&.last || raise("No running Itsi::Server instance")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tls_bindings
|
|
40
|
+
current_server.tls_bindings
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tls_domains(listener_id = nil)
|
|
44
|
+
current_server.tls_domains(listener_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tls_domain_statuses(listener_id = nil)
|
|
48
|
+
current_server.tls_domain_statuses(listener_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register_tls_domain(domain, listener_id = nil)
|
|
52
|
+
current_server.register_tls_domain(domain, listener_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def unregister_tls_domain(domain, listener_id = nil)
|
|
56
|
+
current_server.unregister_tls_domain(domain, listener_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
35
59
|
def start_in_background_thread(cli_params = {}, &blk)
|
|
36
60
|
@background_threads ||= []
|
|
37
61
|
server, background_thread = start(cli_params, background: true, &blk)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "helpers/test_helper"
|
|
4
|
+
require "helpers/local_acme"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
class TestLocalAcmeChallenges < Minitest::Test
|
|
9
|
+
APP = proc { |_env| [200, { "content-type" => "text/plain" }, ["acme-ok"]] }
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def local_acme
|
|
13
|
+
raise Minitest::Skip, "Go is required for local ACME tests" unless LocalAcmeAuthority.available?
|
|
14
|
+
|
|
15
|
+
return @local_acme if @local_acme
|
|
16
|
+
|
|
17
|
+
@local_acme = LocalAcmeAuthority.new
|
|
18
|
+
@local_acme.start
|
|
19
|
+
@previous_env = {}
|
|
20
|
+
@local_acme.env.each do |key, value|
|
|
21
|
+
@previous_env[key] = ENV[key]
|
|
22
|
+
ENV[key] = value
|
|
23
|
+
end
|
|
24
|
+
@local_acme
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shutdown
|
|
28
|
+
return unless @local_acme
|
|
29
|
+
|
|
30
|
+
@previous_env&.each do |key, value|
|
|
31
|
+
value.nil? ? ENV.delete(key) : ENV[key] = value
|
|
32
|
+
end
|
|
33
|
+
@local_acme.stop
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_tls_alpn01_issuance_without_http_listener
|
|
38
|
+
domain = "alpn.itsi.test"
|
|
39
|
+
local_acme = self.class.local_acme
|
|
40
|
+
https_bind = "https://0.0.0.0:#{local_acme.tls_port}?cert=acme&domains=#{domain}&acme_email=test@example.com"
|
|
41
|
+
|
|
42
|
+
server(app: APP, bind: https_bind) do
|
|
43
|
+
wait_for_https_response(local_acme.tls_port, domain)
|
|
44
|
+
|
|
45
|
+
certificate = peer_certificate(local_acme.tls_port, domain)
|
|
46
|
+
assert_certificate_domain(certificate, domain)
|
|
47
|
+
|
|
48
|
+
response = https_get(local_acme.tls_port, "/", domain)
|
|
49
|
+
assert_equal "200", response.code
|
|
50
|
+
assert_equal "acme-ok", response.body
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_http01_issuance_when_tls_validation_port_is_unavailable
|
|
55
|
+
domain = "http01.itsi.test"
|
|
56
|
+
local_acme = self.class.local_acme
|
|
57
|
+
app_port = free_tcp_port
|
|
58
|
+
https_bind = "https://0.0.0.0:#{app_port}?cert=acme&domains=#{domain}&acme_email=test@example.com"
|
|
59
|
+
http_bind = "http://0.0.0.0:#{local_acme.http_port}"
|
|
60
|
+
|
|
61
|
+
server(app: APP, bind: https_bind, binds: [https_bind, http_bind]) do
|
|
62
|
+
wait_for_https_response(app_port, domain)
|
|
63
|
+
|
|
64
|
+
certificate = peer_certificate(app_port, domain)
|
|
65
|
+
assert_certificate_domain(certificate, domain)
|
|
66
|
+
|
|
67
|
+
response = https_get(app_port, "/", domain)
|
|
68
|
+
assert_equal "200", response.code
|
|
69
|
+
assert_equal "acme-ok", response.body
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_dynamic_http01_issuance_with_runtime_domain_registration
|
|
74
|
+
domain = "runtime-http01.itsi.test"
|
|
75
|
+
local_acme = self.class.local_acme
|
|
76
|
+
app_port = free_tcp_port
|
|
77
|
+
https_bind = "https://0.0.0.0:#{app_port}?cert=acme&acme_email=test@example.com"
|
|
78
|
+
http_bind = "http://0.0.0.0:#{local_acme.http_port}"
|
|
79
|
+
|
|
80
|
+
server(app: APP, bind: https_bind, binds: [https_bind, http_bind]) do
|
|
81
|
+
assert_equal [], Itsi::Server.tls_domains
|
|
82
|
+
assert_equal ["tcp://0.0.0.0:#{app_port}"], Itsi::Server.tls_bindings
|
|
83
|
+
|
|
84
|
+
Itsi::Server.register_tls_domain(domain)
|
|
85
|
+
wait_until { Itsi::Server.tls_domains.include?(domain) }
|
|
86
|
+
wait_for_https_response(app_port, domain)
|
|
87
|
+
|
|
88
|
+
statuses = Itsi::Server.tls_domain_statuses
|
|
89
|
+
active = statuses.find { |status| status["domain"] == domain }
|
|
90
|
+
refute_nil active
|
|
91
|
+
assert_equal "active", active["status"]
|
|
92
|
+
|
|
93
|
+
certificate = peer_certificate(app_port, domain)
|
|
94
|
+
assert_certificate_domain(certificate, domain)
|
|
95
|
+
|
|
96
|
+
Itsi::Server.unregister_tls_domain(domain)
|
|
97
|
+
wait_until { !Itsi::Server.tls_domains.include?(domain) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_dynamic_tls_alpn01_issuance_with_runtime_domain_registration
|
|
102
|
+
domain = "runtime-alpn.itsi.test"
|
|
103
|
+
local_acme = self.class.local_acme
|
|
104
|
+
https_bind = "https://0.0.0.0:#{local_acme.tls_port}?cert=acme&acme_email=test@example.com"
|
|
105
|
+
|
|
106
|
+
server(app: APP, bind: https_bind) do
|
|
107
|
+
assert_equal [], Itsi::Server.tls_domains
|
|
108
|
+
|
|
109
|
+
Itsi::Server.register_tls_domain(domain)
|
|
110
|
+
wait_until { Itsi::Server.tls_domains.include?(domain) }
|
|
111
|
+
wait_for_https_response(local_acme.tls_port, domain)
|
|
112
|
+
|
|
113
|
+
statuses = Itsi::Server.tls_domain_statuses
|
|
114
|
+
active = statuses.find { |status| status["domain"] == domain }
|
|
115
|
+
refute_nil active
|
|
116
|
+
assert_equal "active", active["status"]
|
|
117
|
+
|
|
118
|
+
certificate = peer_certificate(local_acme.tls_port, domain)
|
|
119
|
+
assert_certificate_domain(certificate, domain)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def free_tcp_port
|
|
126
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
127
|
+
port = server.addr[1]
|
|
128
|
+
server.close
|
|
129
|
+
port
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def wait_for_https_response(port, host = "127.0.0.1")
|
|
133
|
+
Timeout.timeout(20) do
|
|
134
|
+
loop do
|
|
135
|
+
begin
|
|
136
|
+
response = https_get(port, "/", host)
|
|
137
|
+
return response if response.code == "200"
|
|
138
|
+
rescue StandardError
|
|
139
|
+
sleep 0.1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def wait_until(timeout: 10)
|
|
146
|
+
Timeout.timeout(timeout) do
|
|
147
|
+
loop do
|
|
148
|
+
return if yield
|
|
149
|
+
|
|
150
|
+
sleep 0.05
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def https_get(port, path, host = "127.0.0.1")
|
|
156
|
+
Net::HTTP.start(
|
|
157
|
+
"127.0.0.1",
|
|
158
|
+
port,
|
|
159
|
+
use_ssl: true,
|
|
160
|
+
verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
|
161
|
+
open_timeout: 1,
|
|
162
|
+
read_timeout: 1
|
|
163
|
+
) do |http|
|
|
164
|
+
request = Net::HTTP::Get.new(path)
|
|
165
|
+
request["Host"] = host
|
|
166
|
+
http.request(request)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def peer_certificate(port, hostname)
|
|
171
|
+
tcp_socket = TCPSocket.new("127.0.0.1", port)
|
|
172
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
173
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
174
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
|
175
|
+
ssl_socket.hostname = hostname if ssl_socket.respond_to?(:hostname=)
|
|
176
|
+
ssl_socket.connect
|
|
177
|
+
ssl_socket.peer_cert
|
|
178
|
+
ensure
|
|
179
|
+
ssl_socket&.close
|
|
180
|
+
tcp_socket&.close
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def assert_certificate_domain(certificate, domain)
|
|
184
|
+
alt_names = certificate.extensions
|
|
185
|
+
.select { |extension| extension.oid == "subjectAltName" }
|
|
186
|
+
.flat_map { |extension| extension.value.split(/,\s*/) }
|
|
187
|
+
|
|
188
|
+
assert_includes alt_names, "DNS:#{domain}"
|
|
189
|
+
end
|
|
190
|
+
end
|