itsi 0.2.26 → 0.2.27.rc1
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/Cargo.lock +7 -3
- 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_http_request.rs +9 -0
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
- 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/services/itsi_http_service.rs +46 -2
- 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 +2 -0
- data/gems/server/Gemfile.lock +123 -0
- data/gems/server/Rakefile +18 -5
- data/gems/server/lib/itsi/http_request.rb +10 -0
- data/gems/server/lib/itsi/server/rack_interface.rb +45 -2
- 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 +79 -0
- data/lib/itsi/version.rb +1 -1
- metadata +12 -6
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: ../scheduler
|
|
3
|
+
specs:
|
|
4
|
+
itsi-scheduler (0.2.27.rc1)
|
|
5
|
+
rb_sys (~> 0.9.91)
|
|
6
|
+
|
|
7
|
+
PATH
|
|
8
|
+
remote: .
|
|
9
|
+
specs:
|
|
10
|
+
itsi-server (0.2.27.rc1)
|
|
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
|
+
base64 (0.3.0)
|
|
21
|
+
bigdecimal (4.1.2)
|
|
22
|
+
builder (3.3.0)
|
|
23
|
+
connection_pool (3.0.2)
|
|
24
|
+
google-protobuf (4.30.2)
|
|
25
|
+
bigdecimal
|
|
26
|
+
rake (>= 13)
|
|
27
|
+
googleapis-common-protos-types (1.19.0)
|
|
28
|
+
google-protobuf (>= 3.18, < 5.a)
|
|
29
|
+
grpc (1.78.0)
|
|
30
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
31
|
+
googleapis-common-protos-types (~> 1.0)
|
|
32
|
+
json (2.19.4)
|
|
33
|
+
jwt (3.1.2)
|
|
34
|
+
base64
|
|
35
|
+
language_server-protocol (3.17.0.5)
|
|
36
|
+
logger (1.7.0)
|
|
37
|
+
minitest (5.27.0)
|
|
38
|
+
minitest-reporters (1.8.0)
|
|
39
|
+
ansi
|
|
40
|
+
builder
|
|
41
|
+
minitest (>= 5.0, < 7)
|
|
42
|
+
ruby-progressbar
|
|
43
|
+
net_http_unix (0.2.2)
|
|
44
|
+
prism (1.9.0)
|
|
45
|
+
rack (3.2.6)
|
|
46
|
+
rackup (2.3.1)
|
|
47
|
+
rack (>= 3)
|
|
48
|
+
rake (13.3.1)
|
|
49
|
+
rake-compiler (1.3.0)
|
|
50
|
+
rake
|
|
51
|
+
rake-compiler-dock (1.11.0)
|
|
52
|
+
rb_sys (0.9.126)
|
|
53
|
+
json (>= 2)
|
|
54
|
+
rake-compiler-dock (= 1.11.0)
|
|
55
|
+
rbs (3.10.3)
|
|
56
|
+
logger
|
|
57
|
+
tsort
|
|
58
|
+
redis (5.4.1)
|
|
59
|
+
redis-client (>= 0.22.0)
|
|
60
|
+
redis-client (0.24.0)
|
|
61
|
+
connection_pool
|
|
62
|
+
ruby-lsp (0.26.7)
|
|
63
|
+
language_server-protocol (~> 3.17.0)
|
|
64
|
+
prism (>= 1.2, < 2.0)
|
|
65
|
+
rbs (>= 3, < 5)
|
|
66
|
+
ruby-progressbar (1.13.0)
|
|
67
|
+
tsort (0.2.0)
|
|
68
|
+
|
|
69
|
+
PLATFORMS
|
|
70
|
+
arm64-darwin-24
|
|
71
|
+
ruby
|
|
72
|
+
|
|
73
|
+
DEPENDENCIES
|
|
74
|
+
bundler
|
|
75
|
+
grpc
|
|
76
|
+
itsi-scheduler!
|
|
77
|
+
itsi-server!
|
|
78
|
+
jwt
|
|
79
|
+
minitest (~> 5.16)
|
|
80
|
+
minitest-reporters
|
|
81
|
+
net_http_unix
|
|
82
|
+
rack
|
|
83
|
+
rackup
|
|
84
|
+
rake (~> 13.0)
|
|
85
|
+
rake-compiler
|
|
86
|
+
rb_sys (~> 0.9.91)
|
|
87
|
+
redis
|
|
88
|
+
ruby-lsp
|
|
89
|
+
|
|
90
|
+
CHECKSUMS
|
|
91
|
+
ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
|
|
92
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
93
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
94
|
+
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
|
95
|
+
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
96
|
+
google-protobuf (4.30.2) sha256=0f35168dbeeccf13d928acf6c128cfec17b9a826ae4505246a02c115f4ae16ed
|
|
97
|
+
googleapis-common-protos-types (1.19.0) sha256=aecb76ca5326f8bcc47ab083259bbc4971d07e87f56808af7e210669d9765694
|
|
98
|
+
grpc (1.78.0) sha256=7aaf47b6d7783fb0e7d40fc853d34e802d47aef7b1312862b6e719141b3527c9
|
|
99
|
+
itsi-scheduler (0.2.27.rc1)
|
|
100
|
+
itsi-server (0.2.27.rc1)
|
|
101
|
+
json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
|
|
102
|
+
jwt (3.1.2) sha256=af6991f19a6bb4060d618d9add7a66f0eeb005ac0bc017cd01f63b42e122d535
|
|
103
|
+
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
104
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
105
|
+
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
|
|
106
|
+
minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
|
|
107
|
+
net_http_unix (0.2.2) sha256=98aa0e1e7787f8383e8dd8ff60fc18062c2ddb2aadbc76e92cfb4f95d2c9d71d
|
|
108
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
109
|
+
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
|
|
110
|
+
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
|
111
|
+
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
|
112
|
+
rake-compiler (1.3.0) sha256=eec272ef6d4dad27b36f5cdcf5b9ee4df2193751f4082b095f981ebf9cdf4127
|
|
113
|
+
rake-compiler-dock (1.11.0) sha256=eab51f2cd533eb35cea6b624a75281f047123e70a64c58b607471bb49428f8c2
|
|
114
|
+
rb_sys (0.9.126) sha256=ba958e0b8b4b89eeae0b3d24b64c809eb2c37e0ab0773a49e9b1c2e22c95aef8
|
|
115
|
+
rbs (3.10.3) sha256=70627f3919016134d554e6c99195552ae3ef6020fe034c8e983facc9c192daa6
|
|
116
|
+
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
|
117
|
+
redis-client (0.24.0) sha256=ee65ee39cb2c38608b734566167fd912384f3c1241f59075e22858f23a085dbb
|
|
118
|
+
ruby-lsp (0.26.7) sha256=60a1199fc7e329348d63a2479854f94435725d833eeabf3d539b790185cbf21f
|
|
119
|
+
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
120
|
+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
121
|
+
|
|
122
|
+
BUNDLED WITH
|
|
123
|
+
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
|
|
@@ -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,7 +97,10 @@ 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
|
body_streamer.call(response)
|
|
@@ -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
|
|
@@ -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
|