summaryrefslogtreecommitdiff
path: root/test/net/http/test_https.rb
blob: 1b8841ad11a86f56230780f871b468722faf952b (plain)
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# frozen_string_literal: false
require "test/unit"
begin
  require 'net/https'
  require 'stringio'
  require 'timeout'
  require File.expand_path("utils", File.dirname(__FILE__))
rescue LoadError
  # should skip this test
end

class TestNetHTTPS < Test::Unit::TestCase
  include TestNetHTTPUtils

  def self.read_fixture(key)
    File.read(File.expand_path("../fixtures/#{key}", __dir__))
  end

  CA_CERT = OpenSSL::X509::Certificate.new(read_fixture("cacert.pem"))
  SERVER_KEY = OpenSSL::PKey.read(read_fixture("server.key"))
  SERVER_CERT = OpenSSL::X509::Certificate.new(read_fixture("server.crt"))
  DHPARAMS = OpenSSL::PKey::DH.new(read_fixture("dhparams.pem"))
  TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) }

  CONFIG = {
    'host' => '127.0.0.1',
    'proxy_host' => nil,
    'proxy_port' => nil,
    'ssl_enable' => true,
    'ssl_certificate' => SERVER_CERT,
    'ssl_private_key' => SERVER_KEY,
    'ssl_tmp_dh_callback' => proc { DHPARAMS },
  }

  def test_get
    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.cert_store = TEST_STORE
    certs = []
    http.verify_callback = Proc.new do |preverify_ok, store_ctx|
      certs << store_ctx.current_cert
      preverify_ok
    end
    http.request_get("/") {|res|
      assert_equal($test_net_http_data, res.body)
    }
    # TODO: OpenSSL 1.1.1h seems to yield only SERVER_CERT; need to check the incompatibility
    certs.each do |cert|
      assert_include([SERVER_CERT.to_der, CA_CERT.to_der], cert.to_der)
    end
  rescue SystemCallError
    skip $!
  end

  def test_get_SNI
    http = Net::HTTP.new("localhost", config("port"))
    http.ipaddr = config('host')
    http.use_ssl = true
    http.cert_store = TEST_STORE
    certs = []
    http.verify_callback = Proc.new do |preverify_ok, store_ctx|
      certs << store_ctx.current_cert
      preverify_ok
    end
    http.request_get("/") {|res|
      assert_equal($test_net_http_data, res.body)
    }
    # TODO: OpenSSL 1.1.1h seems to yield only SERVER_CERT; need to check the incompatibility
    certs.each do |cert|
      assert_include([SERVER_CERT.to_der, CA_CERT.to_der], cert.to_der)
    end
  end

  def test_get_SNI_proxy
    TCPServer.open("127.0.0.1", 0) {|serv|
      _, port, _, _ = serv.addr
      client_thread = Thread.new {
        proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password')
        http = proxy.new("foo.example.org", 8000)
        http.ipaddr = "192.0.2.1"
        http.use_ssl = true
        http.cert_store = TEST_STORE
        certs = []
        http.verify_callback = Proc.new do |preverify_ok, store_ctx|
          certs << store_ctx.current_cert
          preverify_ok
        end
        begin
          http.start
        rescue EOFError
        end
      }
      server_thread = Thread.new {
        sock = serv.accept
        begin
          proxy_request = sock.gets("\r\n\r\n")
          assert_equal(
            "CONNECT 192.0.2.1:8000 HTTP/1.1\r\n" +
            "Host: foo.example.org:8000\r\n" +
            "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" +
            "\r\n",
            proxy_request,
            "[ruby-dev:25673]")
        ensure
          sock.close
        end
      }
      assert_join_threads([client_thread, server_thread])
    }

  end

  def test_get_SNI_failure
    TestNetHTTPUtils.clean_http_proxy_env do
      http = Net::HTTP.new("invalidservername", config("port"))
      http.ipaddr = config('host')
      http.use_ssl = true
      http.cert_store = TEST_STORE
      certs = []
      http.verify_callback = Proc.new do |preverify_ok, store_ctx|
        certs << store_ctx.current_cert
        preverify_ok
      end
      @log_tester = lambda {|_| }
      assert_raise(OpenSSL::SSL::SSLError){ http.start }
    end
  end

  def test_post
    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.cert_store = TEST_STORE
    data = config('ssl_private_key').to_der
    http.request_post("/", data, {'content-type' => 'application/x-www-form-urlencoded'}) {|res|
      assert_equal(data, res.body)
    }
  rescue SystemCallError
    skip $!
  end

  def test_session_reuse
    # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h.
    # See https://github.com/openssl/openssl/pull/5967 for details.
    skip if OpenSSL::OPENSSL_LIBRARY_VERSION =~ /OpenSSL 1.1.0h/

    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.cert_store = TEST_STORE

    if OpenSSL::OPENSSL_LIBRARY_VERSION =~ /LibreSSL (\d+\.\d+)/ && $1.to_f > 3.19
      # LibreSSL 3.2 defaults to TLSv1.3 in server and client, which doesn't currently
      # support session resuse.  Limiting the version to the TLSv1.2 stack allows
      # this test to continue to work on LibreSSL 3.2+.  LibreSSL may eventually
      # support session reuse, but there are no current plans to do so.
      http.ssl_version = :TLSv1
    end

    http.start
    http.get("/")
    http.finish

    http.start
    http.get("/")

    socket = http.instance_variable_get(:@socket).io
    assert_equal true, socket.session_reused?

    http.finish
  rescue SystemCallError
    skip $!
  end

  def test_session_reuse_but_expire
    # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h.
    skip if OpenSSL::OPENSSL_LIBRARY_VERSION =~ /OpenSSL 1.1.0h/

    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.cert_store = TEST_STORE

    http.ssl_timeout = -1
    http.start
    http.get("/")
    http.finish

    http.start
    http.get("/")

    socket = http.instance_variable_get(:@socket).io
    assert_equal false, socket.session_reused?

    http.finish
  rescue SystemCallError
    skip $!
  end

  if ENV["RUBY_OPENSSL_TEST_ALL"]
    def test_verify
      http = Net::HTTP.new("ssl.netlab.jp", 443)
      http.use_ssl = true
      assert(
        (http.request_head("/"){|res| } rescue false),
        "The system may not have default CA certificate store."
      )
    end
  end

  def test_verify_none
    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    http.request_get("/") {|res|
      assert_equal($test_net_http_data, res.body)
    }
  rescue SystemCallError
    skip $!
  end

  def test_skip_hostname_verification
    TestNetHTTPUtils.clean_http_proxy_env do
      http = Net::HTTP.new('invalidservername', config('port'))
      http.ipaddr = config('host')
      http.use_ssl = true
      http.cert_store = TEST_STORE
      http.verify_hostname = false
      assert_nothing_raised { http.start }
    ensure
      http.finish if http&.started?
    end
  end

  def test_fail_if_verify_hostname_is_true
    TestNetHTTPUtils.clean_http_proxy_env do
      http = Net::HTTP.new('invalidservername', config('port'))
      http.ipaddr = config('host')
      http.use_ssl = true
      http.cert_store = TEST_STORE
      http.verify_hostname = true
      @log_tester = lambda { |_| }
      assert_raise(OpenSSL::SSL::SSLError) { http.start }
    end
  end

  def test_certificate_verify_failure
    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    ex = assert_raise(OpenSSL::SSL::SSLError){
      begin
        http.request_get("/") {|res| }
      rescue SystemCallError
        skip $!
      end
    }
    assert_match(/certificate verify failed/, ex.message)
    unless /mswin|mingw/ =~ RUBY_PLATFORM
      # on Windows, Errno::ECONNRESET will be raised, and it'll be eaten by
      # WEBrick
      @log_tester = lambda {|log|
        assert_equal(1, log.length)
        assert_match(/ERROR OpenSSL::SSL::SSLError:/, log[0])
      }
    end
  end

  def test_identity_verify_failure
    # the certificate's subject has CN=localhost
    http = Net::HTTP.new("127.0.0.1", config("port"))
    http.use_ssl = true
    http.cert_store = TEST_STORE
    @log_tester = lambda {|_| }
    ex = assert_raise(OpenSSL::SSL::SSLError){
      http.request_get("/") {|res| }
    }
    re_msg = /certificate verify failed|hostname \"127.0.0.1\" does not match/
    assert_match(re_msg, ex.message)
  end

  def test_timeout_during_SSL_handshake
    bug4246 = "expected the SSL connection to have timed out but have not. [ruby-core:34203]"

    # listen for connections... but deliberately do not complete SSL handshake
    TCPServer.open('localhost', 0) {|server|
      port = server.addr[1]

      conn = Net::HTTP.new('localhost', port)
      conn.use_ssl = true
      conn.read_timeout = 0.01
      conn.open_timeout = 0.01

      th = Thread.new do
        assert_raise(Net::OpenTimeout) {
          conn.get('/')
        }
      end
      assert th.join(10), bug4246
    }
  end

  def test_min_version
    http = Net::HTTP.new("localhost", config("port"))
    http.use_ssl = true
    http.min_version = :TLS1
    http.cert_store = TEST_STORE
    http.request_get("/") {|res|
      assert_equal($test_net_http_data, res.body)
    }
  end

  def test_max_version
    http = Net::HTTP.new("127.0.0.1", config("port"))
    http.use_ssl = true
    http.max_version = :SSL2
    http.verify_callback = Proc.new do |preverify_ok, store_ctx|
      true
    end
    @log_tester = lambda {|_| }
    ex = assert_raise(OpenSSL::SSL::SSLError){
      http.request_get("/") {|res| }
    }
    re_msg = /\ASSL_connect returned=1 errno=0 |SSL_CTX_set_max_proto_version/
    assert_match(re_msg, ex.message)
  end

end if defined?(OpenSSL::SSL)