-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
test_integration_ssl.rb
216 lines (181 loc) · 6.28 KB
/
test_integration_ssl.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
require_relative 'helper'
require_relative "helpers/integration"
require "puma/log_writer"
# These tests are used to verify that Puma works with SSL sockets. Only
# integration tests isolate the server from the test environment, so there
# should be a few SSL tests.
#
# For instance, since other tests make use of 'client' SSLSockets created by
# net/http, OpenSSL is loaded in the CI process. By shelling out with IO.popen,
# the server process isn't affected by whatever is loaded in the CI process.
class TestIntegrationSSL < TestIntegration
parallelize_me! if ::Puma.mri?
require "net/http"
require "openssl"
def teardown
@server.close if @server && !@server.closed?
@server = nil
super
end
def bind_port
@bind_port ||= UniquePort.call
end
def control_tcp_port
@control_tcp_port ||= UniquePort.call
end
def with_server(config)
config_file = Tempfile.new %w(config .rb)
config_file.write config
config_file.close
config_file.path
# start server
cmd = "#{BASE} bin/puma -C #{config_file.path}"
@server = IO.popen cmd, 'r'
wait_for_server_to_boot
@pid = @server.pid
http = Net::HTTP.new HOST, bind_port
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
yield http
# stop server
sock = TCPSocket.new HOST, control_tcp_port
@ios_to_close << sock
sock.syswrite "GET /stop?token=#{TOKEN} HTTP/1.1\r\n\r\n"
sock.read
assert_match 'Goodbye!', @server.read
end
def test_ssl_run
config = <<RUBY
if ::Puma.jruby?
keystore = '#{File.expand_path '../examples/puma/keystore.jks', __dir__}'
keystore_pass = 'jruby_puma'
ssl_bind '#{HOST}', '#{bind_port}', {
keystore: keystore,
keystore_pass: keystore_pass,
verify_mode: 'none'
}
else
key = '#{File.expand_path '../examples/puma/puma_keypair.pem', __dir__}'
cert = '#{File.expand_path '../examples/puma/cert_puma.pem', __dir__}'
ssl_bind '#{HOST}', '#{bind_port}', {
cert: cert,
key: key,
verify_mode: 'none'
}
end
activate_control_app 'tcp://#{HOST}:#{control_tcp_port}', { auth_token: '#{TOKEN}' }
app do |env|
[200, {}, [env['rack.url_scheme']]]
end
RUBY
with_server(config) do |http|
body = nil
http.start do
req = Net::HTTP::Get.new '/', {}
http.request(req) { |resp| body = resp.body }
end
assert_equal 'https', body
end
end
def test_ssl_run_with_curl_client
skip_if :windows; require 'stringio'
app = lambda { |_| [200, { 'Content-Type' => 'text/plain' }, ["HELLO", ' ', "THERE"]] }
opts = {max_threads: 1}
server = Puma::Server.new app, Puma::LogWriter.stdio, Puma::Events.new, opts
if Puma.jruby?
ssl_params = {
'keystore' => File.expand_path('../examples/puma/client-certs/keystore.jks', __dir__),
'keystore-pass' => 'jruby_puma', # keystore includes server.p12 as well as ca.crt
}
else
ssl_params = {
'cert' => File.expand_path('../examples/puma/client-certs/server.crt', __dir__),
'key' => File.expand_path('../examples/puma/client-certs/server.key', __dir__),
'ca' => File.expand_path('../examples/puma/client-certs/ca.crt', __dir__),
}
end
ssl_params['verify_mode'] = 'force_peer' # 'peer'
out_err = StringIO.new
ssl_context = Puma::MiniSSL::ContextBuilder.new(ssl_params, Puma::LogWriter.new(out_err, out_err)).context
server.add_ssl_listener(HOST, bind_port, ssl_context)
server.run(true)
begin
ca = File.expand_path('../examples/puma/client-certs/ca.crt', __dir__)
cert = File.expand_path('../examples/puma/client-certs/client.crt', __dir__)
key = File.expand_path('../examples/puma/client-certs/client.key', __dir__)
# NOTE: JRuby used to end up in a hang with TLS peer verification enabled
# it's easier to reproduce using an external client such as CURL (using net/http client the bug isn't triggered)
# also the "hang", being buffering related, seems to showcase better with TLS 1.2 than 1.3
body = curl_and_get_response "https://localhost:#{bind_port}",
args: "--cacert #{ca} --cert #{cert} --key #{key} --tlsv1.2 --tls-max 1.2"
warn out_err.string unless out_err.string.empty?
assert_equal 'HELLO THERE', body
ensure
server.stop(true)
end
assert_equal '', out_err.string
end
def test_ssl_run_with_pem
skip_if :jruby
config = <<RUBY
key_path = '#{File.expand_path '../examples/puma/puma_keypair.pem', __dir__}'
cert_path = '#{File.expand_path '../examples/puma/cert_puma.pem', __dir__}'
ssl_bind '#{HOST}', '#{bind_port}', {
cert_pem: File.read(cert_path),
key_pem: File.read(key_path),
verify_mode: 'none'
}
activate_control_app 'tcp://#{HOST}:#{control_tcp_port}', { auth_token: '#{TOKEN}' }
app do |env|
[200, {}, [env['rack.url_scheme']]]
end
RUBY
with_server(config) do |http|
body = nil
http.start do
req = Net::HTTP::Get.new '/', {}
http.request(req) { |resp| body = resp.body }
end
assert_equal 'https', body
end
end
def test_ssl_run_with_localhost_authority
skip_if :jruby
config = <<RUBY
require 'localhost'
ssl_bind '#{HOST}', '#{bind_port}'
activate_control_app 'tcp://#{HOST}:#{control_tcp_port}', { auth_token: '#{TOKEN}' }
app do |env|
[200, {}, [env['rack.url_scheme']]]
end
RUBY
with_server(config) do |http|
body = nil
http.start do
req = Net::HTTP::Get.new '/', {}
http.request(req) { |resp| body = resp.body }
end
assert_equal 'https', body
end
end
private
def curl_and_get_response(url, method: :get, args: nil); require 'open3'
cmd = "curl -s -v --show-error #{args} -X #{method.to_s.upcase} -k #{url}"
begin
out, err, status = Open3.capture3(cmd)
rescue Errno::ENOENT
fail "curl not available, make sure curl binary is installed and available on $PATH"
end
if status.success?
http_status = err.match(/< HTTP\/1.1 (.*?)/)[1] || '0' # < HTTP/1.1 200 OK\r\n
if http_status.strip[0].to_i > 2
warn out
fail "#{cmd.inspect} unexpected response: #{http_status}\n\n#{err}"
end
return out
else
warn out
fail "#{cmd.inspect} process failed: #{status}\n\n#{err}"
end
end
end if ::Puma::HAS_SSL