diff options
Diffstat (limited to 'test/net/http')
| -rw-r--r-- | test/net/http/test_http.rb | 473 | ||||
| -rw-r--r-- | test/net/http/test_http_request.rb | 53 | ||||
| -rw-r--r-- | test/net/http/test_httpheader.rb | 73 | ||||
| -rw-r--r-- | test/net/http/test_httpresponse.rb | 345 | ||||
| -rw-r--r-- | test/net/http/test_https.rb | 287 | ||||
| -rw-r--r-- | test/net/http/test_https_proxy.rb | 51 | ||||
| -rw-r--r-- | test/net/http/utils.rb | 373 |
7 files changed, 1454 insertions, 201 deletions
diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index f2c03f2915..4e7fa22756 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -34,7 +34,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_class_Proxy_from_ENV - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' # These are ignored on purpose. See Bug 4388 and Feature 6546 @@ -59,6 +59,33 @@ class TestNetHTTP < Test::Unit::TestCase end end + def test_addr_port + http = Net::HTTP.new 'hostname.example', nil, nil + addr_port = http.__send__ :addr_port + assert_equal 'hostname.example', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal 'hostname.example:80', addr_port + + http = Net::HTTP.new '203.0.113.1', nil, nil + addr_port = http.__send__ :addr_port + assert_equal '203.0.113.1', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal '203.0.113.1:80', addr_port + + http = Net::HTTP.new '2001:db8::1', nil, nil + addr_port = http.__send__ :addr_port + assert_equal '[2001:db8::1]', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal '[2001:db8::1]:80', addr_port + + end + def test_edit_path http = Net::HTTP.new 'hostname.example', nil, nil @@ -88,7 +115,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_proxy_address - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do http = Net::HTTP.new 'hostname.example', nil, 'proxy.example' assert_equal 'proxy.example', http.proxy_address @@ -97,8 +124,18 @@ class TestNetHTTP < Test::Unit::TestCase end end + def test_proxy_address_no_proxy + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new 'hostname.example', nil, 'proxy.com', nil, nil, nil, 'example' + assert_nil http.proxy_address + + http = Net::HTTP.new '10.224.1.1', nil, 'proxy.com', nil, nil, nil, 'example,10.224.0.0/22' + assert_nil http.proxy_address + end + end + def test_proxy_from_env_ENV - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' assert_equal false, Net::HTTP.proxy_class? @@ -109,7 +146,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_proxy_address_ENV - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' http = Net::HTTP.new 'hostname.example' @@ -119,13 +156,13 @@ class TestNetHTTP < Test::Unit::TestCase end def test_proxy_eh_no_proxy - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do assert_equal false, Net::HTTP.new('hostname.example', nil, nil).proxy? end end def test_proxy_eh_ENV - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' http = Net::HTTP.new 'hostname.example' @@ -134,14 +171,38 @@ class TestNetHTTP < Test::Unit::TestCase end end + def test_proxy_eh_ENV_with_user + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://foo:bar@proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy? + assert_equal 'foo', http.proxy_user + assert_equal 'bar', http.proxy_pass + end + end + + def test_proxy_eh_ENV_with_urlencoded_user + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy? + assert_equal "Y\\X", http.proxy_user + assert_equal "R%S] ?X", http.proxy_pass + end + end + def test_proxy_eh_ENV_none_set - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do assert_equal false, Net::HTTP.new('hostname.example').proxy? end end def test_proxy_eh_ENV_no_proxy - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' ENV['no_proxy'] = 'hostname.example' @@ -150,7 +211,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_proxy_port - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do http = Net::HTTP.new 'example', nil, 'proxy.example' assert_equal 'proxy.example', http.proxy_address assert_equal 80, http.proxy_port @@ -162,7 +223,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_proxy_port_ENV - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' http = Net::HTTP.new 'hostname.example' @@ -172,7 +233,7 @@ class TestNetHTTP < Test::Unit::TestCase end def test_newobj - clean_http_proxy_env do + TestNetHTTPUtils.clean_http_proxy_env do ENV['http_proxy'] = 'http://proxy.example:8000' http = Net::HTTP.newobj 'hostname.example' @@ -181,25 +242,6 @@ class TestNetHTTP < Test::Unit::TestCase end end - def clean_http_proxy_env - orig = { - 'http_proxy' => ENV['http_proxy'], - 'http_proxy_user' => ENV['http_proxy_user'], - 'http_proxy_pass' => ENV['http_proxy_pass'], - 'no_proxy' => ENV['no_proxy'], - } - - orig.each_key do |key| - ENV.delete key - end - - yield - ensure - orig.each do |key, value| - ENV[key] = value - end - end - def test_failure_message_includes_failed_domain_and_port # hostname to be included in the error message host = Struct.new(:to_s).new("<example>") @@ -208,17 +250,79 @@ class TestNetHTTP < Test::Unit::TestCase def host.to_str; raise SocketError, "open failure"; end uri = Struct.new(:scheme, :hostname, :port).new("http", host, port) assert_raise_with_message(SocketError, /#{host}:#{port}/) do - Net::HTTP.get(uri) + TestNetHTTPUtils.clean_http_proxy_env{ Net::HTTP.get(uri) } end end + def test_default_configuration + Net::HTTP.default_configuration = { open_timeout: 5 } + http = Net::HTTP.new 'hostname.example' + assert_equal 5, http.open_timeout + assert_equal 60, http.read_timeout + + http.open_timeout = 10 + assert_equal 10, http.open_timeout + ensure + Net::HTTP.default_configuration = nil + end + end module TestNetHTTP_version_1_1_methods + def test_s_start + begin + h = Net::HTTP.start(config('host'), config('port')) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal true, h.instance_variable_get(:@proxy_from_env) + + begin + h = Net::HTTP.start(config('host'), config('port'), :ENV) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal true, h.instance_variable_get(:@proxy_from_env) + + begin + h = Net::HTTP.start(config('host'), config('port'), nil) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal false, h.instance_variable_get(:@proxy_from_env) + end + def test_s_get assert_equal $test_net_http_data, Net::HTTP.get(config('host'), '/', config('port')) + + assert_equal $test_net_http_data, Net::HTTP.get( + URI.parse("http://#{config('host')}:#{config('port')}") + ) + assert_equal $test_net_http_data, Net::HTTP.get( + URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" + ) + end + + def test_s_get_response + res = Net::HTTP.get_response( + URI.parse("http://#{config('host')}:#{config('port')}") + ) + assert_equal "application/octet-stream", res["Content-Type"] + assert_equal $test_net_http_data, res.body + + res = Net::HTTP.get_response( + URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" + ) + assert_equal "text/plain", res["Content-Type"] + assert_equal $test_net_http_data, res.body end def test_head @@ -350,7 +454,11 @@ module TestNetHTTP_version_1_1_methods def test_post start {|http| _test_post__base http + } + start {|http| _test_post__file http + } + start {|http| _test_post__no_data http } end @@ -385,12 +493,13 @@ module TestNetHTTP_version_1_1_methods end def test_s_post - url = "http://#{config('host')}:#{config('port')}/" + url = "http://#{config('host')}:#{config('port')}/?q=a" res = Net::HTTP.post( - URI.parse(url), - "a=x") - assert_equal "application/x-www-form-urlencoded", res["Content-Type"] + URI.parse(url), + "a=x") + assert_equal "application/octet-stream", res["Content-Type"] assert_equal "a=x", res.body + assert_equal url, res["X-request-uri"] res = Net::HTTP.post( URI.parse(url), @@ -445,6 +554,57 @@ module TestNetHTTP_version_1_1_methods assert_equal data, res.entity end + def test_timeout_during_HTTP_session_write + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? + conn.open_timeout = EnvUtil.apply_timeout_scale(1) + + th = Thread.new do + err = !windows? ? Net::WriteTimeout : Net::ReadTimeout + assert_raise(err) do + conn.post('/', "a"*50_000_000) + end + end + assert th.join(EnvUtil.apply_timeout_scale(10)) + } + ensure + th&.kill + th&.join + end + + def test_timeout_during_non_chunked_streamed_HTTP_session_write + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? + conn.open_timeout = EnvUtil.apply_timeout_scale(1) + + req = Net::HTTP::Post.new('/') + data = "a"*50_000_000 + req.content_length = data.size + req['Content-Type'] = 'application/x-www-form-urlencoded' + req.body_stream = StringIO.new(data) + + th = Thread.new do + assert_raise(Net::WriteTimeout) { conn.request(req) } + end + assert th.join(10) + } + ensure + th&.kill + th&.join + end + def test_timeout_during_HTTP_session bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" @@ -454,15 +614,15 @@ module TestNetHTTP_version_1_1_methods port = server.addr[1] conn = Net::HTTP.new('localhost', port) - conn.read_timeout = 0.01 - conn.open_timeout = 0.1 + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.open_timeout = EnvUtil.apply_timeout_scale(1) th = Thread.new do assert_raise(Net::ReadTimeout) { conn.get('/') } end - assert th.join(10), bug4246 + assert th.join(EnvUtil.apply_timeout_scale(10)), bug4246 } ensure th.kill @@ -481,10 +641,12 @@ module TestNetHTTP_version_1_2_methods # _test_request__range http # WEBrick does not support Range: header. _test_request__HEAD http _test_request__POST http - _test_request__stream_body http _test_request__uri http _test_request__uri_host http } + start {|http| + _test_request__stream_body http + } end def _test_request__GET(http) @@ -695,7 +857,13 @@ Content-Type: application/octet-stream __EOM__ start {|http| _test_set_form_urlencoded(http, data.reject{|k,v|!v.is_a?(String)}) + } + start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) _test_set_form_multipart(http, false, data, expected) + } + start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) _test_set_form_multipart(http, true, data, expected) } } @@ -739,6 +907,7 @@ __EOM__ expected.sub!(/<filename>/, filename) expected.sub!(/<data>/, $test_net_http_data) start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) data.each{|k,v|v.rewind rescue nil} req = Net::HTTP::Post.new('/') req.set_form(data, 'multipart/form-data') @@ -754,10 +923,11 @@ __EOM__ header) assert_equal(expected, body) - data.each{|k,v|v.rewind rescue nil} - req['Transfer-Encoding'] = 'chunked' - res = http.request req - #assert_equal(expected, res.body) + # TODO: test with chunked + # data.each{|k,v|v.rewind rescue nil} + # req['Transfer-Encoding'] = 'chunked' + # res = http.request req + # assert_equal(expected, res.body) } } end @@ -778,6 +948,17 @@ class TestNetHTTP_v1_2 < Test::Unit::TestCase Net::HTTP.version_1_2 super end + + def test_send_large_POST_request + start {|http| + data = ' '*6000000 + res = http.send_request('POST', '/', data, 'content-type' => 'application/x-www-form-urlencoded') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data.size, res.body.size + assert_equal data, res.body + } + end end class TestNetHTTP_v1_2_chunked < Test::Unit::TestCase @@ -825,7 +1006,7 @@ class TestNetHTTPContinue < Test::Unit::TestCase end def mount_proc(&block) - @server.mount('/continue', WEBrick::HTTPServlet::ProcHandler.new(block.to_proc)) + @server.mount('/continue', block.to_proc) end def test_expect_continue @@ -880,10 +1061,10 @@ class TestNetHTTPContinue < Test::Unit::TestCase def test_expect_continue_error_before_body @log_tester = nil mount_proc {|req, res| - raise WEBrick::HTTPStatus::Forbidden + raise TestNetHTTPUtils::Forbidden } start {|http| - uheader = {'content-length' => '5', 'expect' => '100-continue'} + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'content-length' => '5', 'expect' => '100-continue'} http.continue_timeout = 1 # allow the server to respond before sending http.request_post('/continue', 'data', uheader) {|res| assert_equal(res.code, '403') @@ -925,7 +1106,7 @@ class TestNetHTTPSwitchingProtocols < Test::Unit::TestCase end def mount_proc(&block) - @server.mount('/continue', WEBrick::HTTPServlet::ProcHandler.new(block.to_proc)) + @server.mount('/continue', block.to_proc) end def test_info @@ -935,7 +1116,8 @@ class TestNetHTTPSwitchingProtocols < Test::Unit::TestCase } start {|http| http.continue_timeout = 0.2 - http.request_post('/continue', 'body=BODY') {|res| + http.request_post('/continue', 'body=BODY', + 'content-type' => 'application/x-www-form-urlencoded') {|res| assert_equal('BODY', res.read_body) } } @@ -998,6 +1180,92 @@ class TestNetHTTPKeepAlive < Test::Unit::TestCase } end + def test_keep_alive_reset_on_new_connection + # Using debug log output on accepting connection: + # + # "[2021-04-29 20:36:46] DEBUG accept: 127.0.0.1:50674\n" + @log_tester = nil + @logger_level = :debug + + start {|http| + res = http.get('/') + http.keep_alive_timeout = 1 + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + http.finish + assert_equal 1, @log.grep(/accept/i).size + + sleep 1.5 + http.start + res = http.get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal 2, @log.grep(/accept/i).size + } + end + + class MockSocket + attr_reader :count + def initialize(success_after: nil) + @success_after = success_after + @count = 0 + end + def close + end + def closed? + end + def write(_) + end + def readline + @count += 1 + if @success_after && @success_after <= @count + "HTTP/1.1 200 OK" + else + raise Errno::ECONNRESET + end + end + def readuntil(*_) + "" + end + def read_all(_) + end + end + + def test_http_retry_success + start {|http| + socket = MockSocket.new(success_after: 10) + http.instance_variable_get(:@socket).close + http.instance_variable_set(:@socket, socket) + assert_equal 0, socket.count + http.max_retries = 10 + res = http.get('/') + assert_equal 10, socket.count + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end + + def test_http_retry_failed + start {|http| + socket = MockSocket.new + http.instance_variable_get(:@socket).close + http.instance_variable_set(:@socket, socket) + http.max_retries = 10 + assert_raise(Errno::ECONNRESET){ http.get('/') } + assert_equal 11, socket.count + } + end + + def test_http_retry_failed_with_block + start {|http| + http.max_retries = 10 + called = 0 + assert_raise(Errno::ECONNRESET){ http.get('/'){called += 1; raise Errno::ECONNRESET} } + assert_equal 1, called + } + @log_tester = nil + end + def test_keep_alive_server_close def @server.run(sock) sock.close @@ -1048,3 +1316,112 @@ class TestNetHTTPLocalBind < Test::Unit::TestCase end end +class TestNetHTTPForceEncoding < Test::Unit::TestCase + CONFIG = { + 'host' => 'localhost', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def fe_request(force_enc, content_type=nil) + @server.mount_proc('/fe') do |req, res| + res['Content-Type'] = content_type if content_type + res.body = "hello\u1234" + end + + http = Net::HTTP.new(config('host'), config('port')) + http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address + assert_not_nil(http.local_host) + assert_nil(http.local_port) + + http.response_body_encoding = force_enc + http.get('/fe') + end + + def test_response_body_encoding_false + res = fe_request(false) + assert_equal("hello\u1234".b, res.body) + assert_equal(Encoding::ASCII_8BIT, res.body.encoding) + end + + def test_response_body_encoding_true_without_content_type + res = fe_request(true) + assert_equal("hello\u1234".b, res.body) + assert_equal(Encoding::ASCII_8BIT, res.body.encoding) + end + + def test_response_body_encoding_true_with_content_type + res = fe_request(true, 'text/html; charset=utf-8') + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end + + def test_response_body_encoding_string_without_content_type + res = fe_request('utf-8') + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end + + def test_response_body_encoding_encoding_without_content_type + res = fe_request(Encoding::UTF_8) + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end +end + +class TestNetHTTPPartialResponse < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_partial_response + str = "0123456789" + @server.mount_proc('/') do |req, res| + res.status = 200 + res['Content-Type'] = 'text/plain' + + res.body = str + res['Content-Length'] = str.length + 1 + end + @server.mount_proc('/show_ip') { |req, res| res.body = req.remote_ip } + + http = Net::HTTP.new(config('host'), config('port')) + res = http.get('/') + assert_equal(str, res.body) + + http = Net::HTTP.new(config('host'), config('port')) + http.ignore_eof = false + assert_raise(EOFError) {http.get('/')} + end +end + +class TestNetHTTPInRactor < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_get + assert_ractor(<<~RUBY, require: 'net/http') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{config('host').dump} + port = #{config('port')} + Net::HTTP.start(host, port) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end +end if defined?(Ractor) && Ractor.method_defined?(:value) diff --git a/test/net/http/test_http_request.rb b/test/net/http/test_http_request.rb index c2144d86c7..9f5cf4f8f5 100644 --- a/test/net/http/test_http_request.rb +++ b/test/net/http/test_http_request.rb @@ -1,7 +1,6 @@ # frozen_string_literal: false require 'net/http' require 'test/unit' -require 'stringio' class HTTPRequestTest < Test::Unit::TestCase @@ -47,8 +46,9 @@ class HTTPRequestTest < Test::Unit::TestCase assert_not_predicate req, :response_body_permitted? expected = { - 'accept' => %w[*/*], - 'user-agent' => %w[Ruby], + 'accept' => %w[*/*], + "accept-encoding" => %w[gzip;q=1.0,deflate;q=0.6,identity;q=0.3], + 'user-agent' => %w[Ruby], } assert_equal expected, req.to_hash @@ -65,6 +65,31 @@ class HTTPRequestTest < Test::Unit::TestCase 'Bug #7381 - do not decode content if the user overrides' end if Net::HTTP::HAVE_ZLIB + def test_initialize_GET_uri + req = Net::HTTP::Get.new(URI("http://example.com/foo")) + assert_equal "/foo", req.path + assert_equal "example.com", req['Host'] + + req = Net::HTTP::Get.new(URI("https://example.com/foo")) + assert_equal "/foo", req.path + assert_equal "example.com", req['Host'] + + req = Net::HTTP::Get.new(URI("https://203.0.113.1/foo")) + assert_equal "/foo", req.path + assert_equal "203.0.113.1", req['Host'] + + req = Net::HTTP::Get.new(URI("https://203.0.113.1:8000/foo")) + assert_equal "/foo", req.path + assert_equal "203.0.113.1:8000", req['Host'] + + req = Net::HTTP::Get.new(URI("https://[2001:db8::1]:8000/foo")) + assert_equal "/foo", req.path + assert_equal "[2001:db8::1]:8000", req['Host'] + + assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("urn:ietf:rfc:7231")) } + assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("http://")) } + end + def test_header_set req = Net::HTTP::Get.new '/' @@ -76,5 +101,25 @@ class HTTPRequestTest < Test::Unit::TestCase 'Bug #7831 - do not decode content if the user overrides' end if Net::HTTP::HAVE_ZLIB + def test_update_uri + req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1")) + req.update_uri("test", 8080, false) + assert_equal "203.0.113.1", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1:2020")) + req.update_uri("test", 8080, false) + assert_equal "203.0.113.1", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]")) + req.update_uri("test", 8080, false) + assert_equal "[2001:db8::1]", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]:2020")) + req.update_uri("test", 8080, false) + assert_equal "[2001:db8::1]", req.uri.host + assert_equal 8080, req.uri.port + end end - diff --git a/test/net/http/test_httpheader.rb b/test/net/http/test_httpheader.rb index 99c47cac93..69563168db 100644 --- a/test/net/http/test_httpheader.rb +++ b/test/net/http/test_httpheader.rb @@ -16,6 +16,30 @@ class HTTPHeaderTest < Test::Unit::TestCase @c = C.new end + def test_initialize + @c.initialize_http_header("foo"=>"abc") + assert_equal "abc", @c["foo"] + @c.initialize_http_header("foo"=>"abc", "bar"=>"xyz") + assert_equal "xyz", @c["bar"] + @c.initialize_http_header([["foo", "abc"]]) + assert_equal "abc", @c["foo"] + @c.initialize_http_header([["foo", "abc"], ["bar","xyz"]]) + assert_equal "xyz", @c["bar"] + assert_raise(NoMethodError){ @c.initialize_http_header("foo"=>[]) } + assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\nb") } + assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\rb") } + end + + def test_initialize_with_broken_coderange + error = RUBY_VERSION >= "3.2" ? Encoding::CompatibilityError : ArgumentError + assert_raise(error){ @c.initialize_http_header("foo"=>"a\xff") } + end + + def test_initialize_with_symbol + @c.initialize_http_header(foo: "abc") + assert_equal "abc", @c["foo"] + end + def test_size assert_equal 0, @c.size @c['a'] = 'a' @@ -40,6 +64,16 @@ class HTTPHeaderTest < Test::Unit::TestCase @c['aaA'] = 'aaa' @c['AAa'] = 'aaa' assert_equal 2, @c.length + + @c['aaa'] = ['aaa', ['bbb', [3]]] + assert_equal 2, @c.length + assert_equal ['aaa', 'bbb', '3'], @c.get_fields('aaa') + + @c['aaa'] = "aaa\xff" + assert_equal 2, @c.length + + assert_raise(ArgumentError){ @c['foo'] = "a\nb" } + assert_raise(ArgumentError){ @c['foo'] = ["a\nb"] } end def test_AREF @@ -65,6 +99,10 @@ class HTTPHeaderTest < Test::Unit::TestCase @c.add_field 'My-Header', 'd, d' assert_equal 'a, b, c, d, d', @c['My-Header'] assert_equal ['a', 'b', 'c', 'd, d'], @c.get_fields('My-Header') + assert_raise(ArgumentError){ @c.add_field 'My-Header', "d\nd" } + @c.add_field 'My-Header', ['e', ["\xff", 7]] + assert_equal "a, b, c, d, d, e, \xff, 7", @c['My-Header'] + assert_equal ['a', 'b', 'c', 'd, d', 'e', "\xff", '7'], @c.get_fields('My-Header') end def test_get_fields @@ -85,7 +123,19 @@ class HTTPHeaderTest < Test::Unit::TestCase class D; include Net::HTTPHeader; end def test_nil_variable_header - assert_nothing_raised { D.new.initialize_http_header({Authorization: nil}) } + assert_nothing_raised do + assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: nil HTTP header: Authorization\n") do + D.new.initialize_http_header({Authorization: nil}) + end + end + end + + def test_duplicated_variable_header + assert_nothing_raised do + assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: duplicated HTTP header: Authorization\n") do + D.new.initialize_http_header({"AUTHORIZATION": "yes", "Authorization": "no"}) + end + end end def test_delete @@ -262,6 +312,18 @@ class HTTPHeaderTest < Test::Unit::TestCase end def test_content_range + @c['Content-Range'] = "bytes 0-499/1000" + assert_equal 0..499, @c.content_range + @c['Content-Range'] = "bytes 1-500/1000" + assert_equal 1..500, @c.content_range + @c['Content-Range'] = "bytes 1-1/1000" + assert_equal 1..1, @c.content_range + @c['Content-Range'] = "tokens 1-1/1000" + assert_equal nil, @c.content_range + + try_invalid_content_range "invalid" + try_invalid_content_range "bytes 123-abc" + try_invalid_content_range "bytes abc-123" end def test_range_length @@ -271,6 +333,15 @@ class HTTPHeaderTest < Test::Unit::TestCase assert_equal 500, @c.range_length @c['Content-Range'] = "bytes 1-1/1000" assert_equal 1, @c.range_length + @c['Content-Range'] = "tokens 1-1/1000" + assert_equal nil, @c.range_length + + try_invalid_content_range "bytes 1-1/abc" + end + + def try_invalid_content_range(s) + @c['Content-Range'] = "#{s}" + assert_raise(Net::HTTPHeaderSyntaxError, s){ @c.content_range } end def test_chunked? diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index a67add7c88..01281063cd 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -54,6 +54,241 @@ EOS assert_equal 'hello', body end + def test_read_body_body_encoding_false + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".b, body + assert_equal Encoding::ASCII_8BIT, body.encoding + end + + def test_read_body_body_encoding_encoding + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = Encoding.find('utf-8') + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_string + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = 'utf-8' + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_without_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".b, body + assert_equal Encoding::ASCII_8BIT, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} +Content-Type: text/plain; charset=utf-8 + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso_8859_1_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} +Content-Type: text/plain; charset=iso-8859-1 + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_meta_charset + res_body = "<html><meta charset=\"utf-8\">hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body, body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso8859_1_meta_charset + res_body = "<html><meta charset=\"iso-8859-1\">hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body.force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_meta_content_charset + res_body = "<meta http-equiv='content-type' content='text/html; charset=UTF-8'>hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body, body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso8859_1_meta_content_charset + res_body = "<meta http-equiv='content-type' content='text/html; charset=ISO-8859-1'>hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body.force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + def test_read_body_block io = dummy_io(<<EOS) HTTP/1.1 200 OK @@ -76,6 +311,36 @@ EOS assert_equal 'hello', body end + def test_read_body_block_mod + # http://ci.rvm.jp/results/trunk-rjit-wait@silicon-docker/3019353 + if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? + omit 'too unstable with --jit-wait, and extending read_timeout did not help it' + end + IO.pipe do |r, w| + buf = 'x' * 1024 + buf.freeze + n = 1024 + len = n * buf.size + th = Thread.new do + w.write("HTTP/1.1 200 OK\r\nContent-Length: #{len}\r\n\r\n") + n.times { w.write(buf) } + :ok + end + io = Net::BufferedIO.new(r) + res = Net::HTTPResponse.read_new(io) + nr = 0 + res.reading_body io, true do + # should be allowed to modify the chunk given to them: + res.read_body do |chunk| + nr += chunk.size + chunk.clear + end + end + assert_equal len, nr + assert_equal :ok, th.value + end + end + def test_read_body_content_encoding_deflate io = dummy_io(<<EOS) HTTP/1.1 200 OK @@ -97,9 +362,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal '5', res['content-length'] assert_equal 'hello', body else assert_equal 'deflate', res['content-encoding'] + assert_equal '13', res['content-length'] assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body end end @@ -125,9 +392,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal '5', res['content-length'] assert_equal 'hello', body else assert_equal 'DEFLATE', res['content-encoding'] + assert_equal '13', res['content-length'] assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body end end @@ -158,9 +427,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal 'hello', body else assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body end end @@ -185,6 +456,7 @@ EOS end assert_equal 'deflate', res['content-encoding'], 'Bug #7831' + assert_equal '13', res['content-length'] assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body, 'Bug #7381' end @@ -208,9 +480,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal 'hello', body else assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15\r\n", body end end @@ -258,9 +532,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal '0', res['content-length'] assert_equal '', body else assert_equal 'deflate', res['content-encoding'] + assert_equal '0', res['content-length'] assert_equal '', body end end @@ -284,9 +560,11 @@ EOS if Net::HTTP::HAVE_ZLIB assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal '', body else assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] assert_equal '', body end end @@ -311,6 +589,41 @@ EOS assert_equal 'hello', body end + def test_read_body_receiving_no_body + io = dummy_io(<<EOS) +HTTP/1.1 204 OK +Connection: close + +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = 'utf-8' + + body = 'something to override' + + res.reading_body io, true do + body = res.read_body + end + + assert_equal nil, body + assert_equal nil, res.body + end + + def test_read_body_outside_of_reading_body + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: 0 + +EOS + + res = Net::HTTPResponse.read_new(io) + + assert_raise IOError do + res.read_body + end + end + def test_uri_equals uri = URI 'http://example' @@ -396,11 +709,41 @@ EOS res = Net::HTTPResponse.read_new(io) assert_equal(nil, res.message) - assert_raise Net::HTTPServerException do + assert_raise Net::HTTPClientException do res.error! end end + def test_read_code_type + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + assert_equal Net::HTTPUnknownResponse, res.code_type + + res = Net::HTTPInformation.new('1.0', '1xx', 'test response') + assert_equal Net::HTTPInformation, res.code_type + + res = Net::HTTPSuccess.new('1.0', '2xx', 'test response') + assert_equal Net::HTTPSuccess, res.code_type + + res = Net::HTTPRedirection.new('1.0', '3xx', 'test response') + assert_equal Net::HTTPRedirection, res.code_type + + res = Net::HTTPClientError.new('1.0', '4xx', 'test response') + assert_equal Net::HTTPClientError, res.code_type + + res = Net::HTTPServerError.new('1.0', '5xx', 'test response') + assert_equal Net::HTTPServerError, res.code_type + end + + def test_inspect_response + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + assert_equal '#<Net::HTTPUnknownResponse ??? test response readbody=false>', res.inspect + + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + socket = Net::BufferedIO.new(StringIO.new('test body')) + res.reading_body(socket, true) {} + assert_equal '#<Net::HTTPUnknownResponse ??? test response readbody=true>', res.inspect + end + private def dummy_io(str) diff --git a/test/net/http/test_https.rb b/test/net/http/test_https.rb index a863dc88fe..f5b21b901f 100644 --- a/test/net/http/test_https.rb +++ b/test/net/http/test_https.rb @@ -1,119 +1,156 @@ # frozen_string_literal: false require "test/unit" +require_relative "utils" begin require 'net/https' - require 'stringio' - require 'timeout' - require File.expand_path("utils", File.dirname(__FILE__)) rescue LoadError # should skip this test end +return unless defined?(OpenSSL::SSL) + class TestNetHTTPS < Test::Unit::TestCase include TestNetHTTPUtils - def self.fixture(key) + def self.read_fixture(key) File.read(File.expand_path("../fixtures/#{key}", __dir__)) end - CA_CERT = OpenSSL::X509::Certificate.new(fixture("cacert.pem")) - SERVER_KEY = OpenSSL::PKey.read(fixture("server.key")) - SERVER_CERT = OpenSSL::X509::Certificate.new(fixture("server.crt")) - DHPARAMS = OpenSSL::PKey::DH.new(fixture("dhparams.pem")) + HOST = 'localhost' + HOST_IP = '127.0.0.1' + 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")) TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } CONFIG = { - 'host' => '127.0.0.1', + 'host' => HOST, '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 = Net::HTTP.new(HOST, 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) + assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) + } + end + + def test_get_SNI + http = Net::HTTP.new(HOST, config("port")) + http.ipaddr = config('host') + http.use_ssl = true + http.cert_store = TEST_STORE + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) + } + end + + def test_get_SNI_proxy + TCPServer.open(HOST_IP, 0) {|serv| + _, port, _, _ = serv.addr + client_thread = Thread.new { + proxy = Net::HTTP.Proxy(HOST_IP, 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 + 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]) } - assert_equal(CA_CERT.to_der, certs[0].to_der) - assert_equal(SERVER_CERT.to_der, certs[1].to_der) - rescue SystemCallError - skip $! + + 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 + @log_tester = lambda {|_| } + assert_raise(OpenSSL::SSL::SSLError){ http.start } + end end def test_post - http = Net::HTTP.new("localhost", config("port")) + http = Net::HTTP.new(HOST, 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 - http = Net::HTTP.new("localhost", config("port")) + http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true http.cert_store = TEST_STORE - http.start - http.get("/") - http.finish + 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_2 + end http.start + session_reused = http.instance_variable_get(:@socket).io.session_reused? + assert_false session_reused unless session_reused.nil? # can not detect re-use under JRuby http.get("/") - http.finish # three times due to possible bug in OpenSSL 0.9.8 - - sid = http.instance_variable_get(:@ssl_session).id + http.finish http.start - http.get("/") - - socket = http.instance_variable_get(:@socket).io - - assert socket.session_reused? - - assert_equal sid, http.instance_variable_get(:@ssl_session).id - + session_reused = http.instance_variable_get(:@socket).io.session_reused? + assert_true session_reused unless session_reused.nil? # can not detect re-use under JRuby + assert_equal $test_net_http_data, http.get("/").body http.finish - rescue SystemCallError - skip $! end def test_session_reuse_but_expire - http = Net::HTTP.new("localhost", config("port")) + http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true http.cert_store = TEST_STORE - http.ssl_timeout = -1 + http.ssl_timeout = 1 http.start http.get("/") http.finish - - sid = http.instance_variable_get(:@ssl_session).id - + sleep 1.25 http.start http.get("/") socket = http.instance_variable_get(:@socket).io - assert_equal false, socket.session_reused? - - assert_not_equal sid, http.instance_variable_get(:@ssl_session).id + assert_equal false, socket.session_reused?, "NOTE: OpenSSL library version is #{OpenSSL::OPENSSL_LIBRARY_VERSION}" http.finish - rescue SystemCallError - skip $! end if ENV["RUBY_OPENSSL_TEST_ALL"] @@ -128,57 +165,71 @@ class TestNetHTTPS < Test::Unit::TestCase end def test_verify_none - http = Net::HTTP.new("localhost", config("port")) + http = Net::HTTP.new(HOST, 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 = Net::HTTP.new(HOST, config("port")) http.use_ssl = true ex = assert_raise(OpenSSL::SSL::SSLError){ - begin - http.request_get("/") {|res| } - rescue SystemCallError - skip $! - end + http.request_get("/") {|res| } } 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 - http = Net::HTTP.new("127.0.0.1", config("port")) + def test_verify_callback + http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true - http.verify_callback = Proc.new do |preverify_ok, store_ctx| - true - end - ex = assert_raise(OpenSSL::SSL::SSLError){ - http.request_get("/") {|res| } + http.cert_store = TEST_STORE + certs = [] + http.verify_callback = Proc.new {|preverify_ok, store_ctx| + certs << store_ctx.current_cert + preverify_ok } - assert_match(/hostname \"127.0.0.1\" does not match/, ex.message) + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + } + assert_equal(SERVER_CERT.to_der, certs.last.to_der) 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| + TCPServer.open(HOST, 0) {|server| port = server.addr[1] - conn = Net::HTTP.new('localhost', port) + conn = Net::HTTP.new(HOST, port) conn.use_ssl = true conn.read_timeout = 0.01 conn.open_timeout = 0.01 @@ -191,4 +242,84 @@ class TestNetHTTPS < Test::Unit::TestCase assert th.join(10), bug4246 } end -end if defined?(OpenSSL::SSL) + + def test_min_version + http = Net::HTTP.new(HOST, 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(HOST, config("port")) + http.use_ssl = true + http.max_version = :SSL2 + http.cert_store = TEST_STORE + @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|No appropriate protocol/ + assert_match(re_msg, ex.message) + end + + def test_ractor + assert_ractor(<<~RUBY, require: 'net/https') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{HOST.dump} + port = #{config('port')} + ca_cert_pem = #{CA_CERT.to_pem.dump} + cert_store = OpenSSL::X509::Store.new.tap { |s| + s.add_cert(OpenSSL::X509::Certificate.new(ca_cert_pem)) + } + Net::HTTP.start(host, port, use_ssl: true, cert_store: cert_store) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end if defined?(Ractor) && Ractor.method_defined?(:value) +end + +class TestNetHTTPSIdentityVerifyFailure < Test::Unit::TestCase + include TestNetHTTPUtils + + def self.read_fixture(key) + File.read(File.expand_path("../fixtures/#{key}", __dir__)) + end + + HOST = 'localhost' + HOST_IP = '127.0.0.1' + 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")) + TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } + + CONFIG = { + 'host' => HOST_IP, + 'proxy_host' => nil, + 'proxy_port' => nil, + 'ssl_enable' => true, + 'ssl_certificate' => SERVER_CERT, + 'ssl_private_key' => SERVER_KEY, + } + + def test_identity_verify_failure + # the certificate's subject has CN=localhost + http = Net::HTTP.new(HOST_IP, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + @log_tester = lambda {|_| } + ex = assert_raise(OpenSSL::SSL::SSLError){ + http.request_get("/") {|res| } + sleep 0.5 + } + re_msg = /certificate verify failed|hostname \"#{HOST_IP}\" does not match/ + assert_match(re_msg, ex.message) + end +end diff --git a/test/net/http/test_https_proxy.rb b/test/net/http/test_https_proxy.rb index f833f1a1e3..237c16e64d 100644 --- a/test/net/http/test_https_proxy.rb +++ b/test/net/http/test_https_proxy.rb @@ -5,14 +5,10 @@ rescue LoadError end require 'test/unit' +return unless defined?(OpenSSL::SSL) + class HTTPSProxyTest < Test::Unit::TestCase def test_https_proxy_authentication - begin - OpenSSL - rescue LoadError - skip 'autoload problem. see [ruby-dev:45021][Bug #5786]' - end - TCPServer.open("127.0.0.1", 0) {|serv| _, port, _, _ = serv.addr client_thread = Thread.new { @@ -43,5 +39,46 @@ class HTTPSProxyTest < Test::Unit::TestCase assert_join_threads([client_thread, server_thread]) } end -end if defined?(OpenSSL) + + def read_fixture(key) + File.read(File.expand_path("../fixtures/#{key}", __dir__)) + end + + def test_https_proxy_ssl_connection + TCPServer.open("127.0.0.1", 0) {|tcpserver| + ctx = OpenSSL::SSL::SSLContext.new + ctx.key = OpenSSL::PKey.read(read_fixture("server.key")) + ctx.cert = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) + serv = OpenSSL::SSL::SSLServer.new(tcpserver, ctx) + + _, port, _, _ = serv.addr + client_thread = Thread.new { + proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password', true) + http = proxy.new("foo.example.org", 8000) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + 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 foo.example.org:8000 HTTP/1.1\r\n" + + "Host: foo.example.org:8000\r\n" + + "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + + "\r\n", + proxy_request, + "[ruby-core:96672]") + ensure + sock.close + end + } + assert_join_threads([client_thread, server_thread]) + } + end +end diff --git a/test/net/http/utils.rb b/test/net/http/utils.rb index dbfd112f31..0b9e440e7c 100644 --- a/test/net/http/utils.rb +++ b/test/net/http/utils.rb @@ -1,13 +1,234 @@ # frozen_string_literal: false -require 'webrick' -begin - require "webrick/https" -rescue LoadError - # SSL features cannot be tested -end -require 'webrick/httpservlet/abstract' +require 'socket' module TestNetHTTPUtils + + class Forbidden < StandardError; end + + class HTTPServer + def initialize(config, &block) + @config = config + @server = TCPServer.new(@config['host'], 0) + @port = @server.addr[1] + @procs = {} + + if @config['ssl_enable'] + require 'openssl' + context = OpenSSL::SSL::SSLContext.new + context.cert = @config['ssl_certificate'] + context.key = @config['ssl_private_key'] + @ssl_server = OpenSSL::SSL::SSLServer.new(@server, context) + end + + @block = block + end + + def start + @thread = Thread.new do + loop do + socket = (@ssl_server || @server).accept + run(socket) + rescue + ensure + socket&.close + end + ensure + (@ssl_server || @server).close + end + end + + def run(socket) + handle_request(socket) + end + + def shutdown + @thread&.kill + @thread&.join + end + + def mount(path, proc) + @procs[path] = proc + end + + def mount_proc(path, &block) + mount(path, block.to_proc) + end + + def handle_request(socket) + request_line = socket.gets + return if request_line.nil? || request_line.strip.empty? + + method, path, _version = request_line.split + headers = {} + while (line = socket.gets) + break if line.strip.empty? + key, value = line.split(': ', 2) + headers[key] = value.strip + end + + if headers['Expect'] == '100-continue' + socket.write "HTTP/1.1 100 Continue\r\n\r\n" + end + + # Set default Content-Type if not provided + if !headers['Content-Type'] && (method == 'POST' || method == 'PUT' || method == 'PATCH') + headers['Content-Type'] = 'application/octet-stream' + end + + req = Request.new(method, path, headers, socket) + if @procs.key?(req.path) || @procs.key?("#{req.path}/") + proc = @procs[req.path] || @procs["#{req.path}/"] + res = Response.new(socket) + begin + proc.call(req, res) + rescue Forbidden + res.status = 403 + end + res.finish + else + @block.call(method, path, headers, socket) + end + end + + def port + @port + end + + class Request + attr_reader :method, :path, :headers, :query, :body + def initialize(method, path, headers, socket) + @method = method + @path, @query = parse_path_and_query(path) + @headers = headers + @socket = socket + if method == 'POST' && (@path == '/continue' || @headers['Content-Type'].include?('multipart/form-data')) + if @headers['Transfer-Encoding'] == 'chunked' + @body = read_chunked_body + else + @body = read_body + end + @query = @body.split('&').each_with_object({}) do |pair, hash| + key, value = pair.split('=') + hash[key] = value + end if @body && @body.include?('=') + end + end + + def [](key) + @headers[key.downcase] + end + + def []=(key, value) + @headers[key.downcase] = value + end + + def continue + @socket.write "HTTP\/1.1 100 continue\r\n\r\n" + end + + def remote_ip + @socket.peeraddr[3] + end + + def peeraddr + @socket.peeraddr + end + + private + + def parse_path_and_query(path) + path, query_string = path.split('?', 2) + query = {} + if query_string + query_string.split('&').each do |pair| + key, value = pair.split('=', 2) + query[key] = value + end + end + [path, query] + end + + def read_body + content_length = @headers['Content-Length']&.to_i + return unless content_length && content_length > 0 + @socket.read(content_length) + end + + def read_chunked_body + body = "" + while (chunk_size = @socket.gets.strip.to_i(16)) > 0 + body << @socket.read(chunk_size) + @socket.read(2) # read \r\n after each chunk + end + body + end + end + + class Response + attr_accessor :body, :headers, :status, :chunked, :cookies + def initialize(client) + @client = client + @body = "" + @headers = {} + @status = 200 + @chunked = false + @cookies = [] + end + + def [](key) + @headers[key.downcase] + end + + def []=(key, value) + @headers[key.downcase] = value + end + + def write_chunk(chunk) + return unless @chunked + @client.write("#{chunk.bytesize.to_s(16)}\r\n") + @client.write("#{chunk}\r\n") + end + + def finish + @client.write build_response_headers + if @chunked + write_chunk(@body) + @client.write "0\r\n\r\n" + else + @client.write @body + end + end + + private + + def build_response_headers + response = "HTTP/1.1 #{@status} #{status_message(@status)}\r\n" + if @chunked + @headers['Transfer-Encoding'] = 'chunked' + else + @headers['Content-Length'] = @body.bytesize.to_s + end + @headers.each do |key, value| + response << "#{key}: #{value}\r\n" + end + @cookies.each do |cookie| + response << "Set-Cookie: #{cookie}\r\n" + end + response << "\r\n" + response + end + + def status_message(code) + case code + when 200 then 'OK' + when 301 then 'Moved Permanently' + when 403 then 'Forbidden' + else 'Unknown' + end + end + end + end + def start(&block) new().start(&block) end @@ -15,7 +236,7 @@ module TestNetHTTPUtils def new klass = Net::HTTP::Proxy(config('proxy_host'), config('proxy_port')) http = klass.new(config('host'), config('port')) - http.set_debug_output logfile() + http.set_debug_output logfile http end @@ -25,7 +246,7 @@ module TestNetHTTPUtils end def logfile - $DEBUG ? $stderr : NullWriter.new + $stderr if $DEBUG end def setup @@ -33,78 +254,106 @@ module TestNetHTTPUtils end def teardown + sleep 0.5 if @config['ssl_enable'] if @server @server.shutdown - @server_thread.join - WEBrick::Utils::TimeoutHandler.terminate end @log_tester.call(@log) if @log_tester - # resume global state Net::HTTP.version_1_2 end def spawn_server @log = [] - @log_tester = lambda {|log| assert_equal([], log ) } + @log_tester = lambda {|log| assert_equal([], log) } @config = self.class::CONFIG - server_config = { - :BindAddress => config('host'), - :Port => 0, - :Logger => WEBrick::Log.new(@log, WEBrick::BasicLog::WARN), - :AccessLog => [], - :ServerType => Thread, - } - server_config[:OutputBufferSize] = 4 if config('chunked') - server_config[:RequestTimeout] = config('RequestTimeout') if config('RequestTimeout') - if defined?(OpenSSL) and config('ssl_enable') - server_config.update({ - :SSLEnable => true, - :SSLCertificate => config('ssl_certificate'), - :SSLPrivateKey => config('ssl_private_key'), - :SSLTmpDhCallback => config('ssl_tmp_dh_callback'), - }) - end - @server = WEBrick::HTTPServer.new(server_config) - @server.mount('/', Servlet, config('chunked')) - @server_thread = @server.start - @config['port'] = @server[:Port] + @server = HTTPServer.new(@config) do |method, path, headers, socket| + @log << "DEBUG accept: #{@config['host']}:#{socket.addr[1]}" if @logger_level == :debug + case method + when 'HEAD' + handle_head(path, headers, socket) + when 'GET' + handle_get(path, headers, socket) + when 'POST' + handle_post(path, headers, socket) + when 'PATCH' + handle_patch(path, headers, socket) + else + socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" + end + end + @server.start + @config['port'] = @server.port + end + + def handle_head(path, headers, socket) + if headers['Accept'] != '*/*' + content_type = headers['Accept'] + else + content_type = $test_net_http_data_type + end + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}" + socket.print(response) + end + + def handle_get(path, headers, socket) + if headers['Accept'] != '*/*' + content_type = headers['Accept'] + else + content_type = $test_net_http_data_type + end + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}\r\n\r\n#{$test_net_http_data}" + socket.print(response) + end + + def handle_post(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + scheme = headers['X-Request-Scheme'] || 'http' + host = @config['host'] + port = socket.addr[1] + content_type = headers['Content-Type'] || 'application/octet-stream' + charset = parse_content_type(content_type)[1] + path = "#{scheme}://#{host}:#{port}#{path}" + path = path.encode(charset) if charset + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\nX-request-uri: #{path}\r\n\r\n#{body}" + socket.print(response) + end + + def handle_patch(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + content_type = headers['Content-Type'] || 'application/octet-stream' + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}" + socket.print(response) + end + + def parse_content_type(content_type) + return [nil, nil] unless content_type + type, *params = content_type.split(';').map(&:strip) + charset = params.find { |param| param.start_with?('charset=') } + charset = charset.split('=', 2).last if charset + [type, charset] end $test_net_http = nil - $test_net_http_data = (0...256).to_a.map {|i| i.chr }.join('') * 64 + $test_net_http_data = (0...256).to_a.map { |i| i.chr }.join('') * 64 $test_net_http_data.force_encoding("ASCII-8BIT") $test_net_http_data_type = 'application/octet-stream' - class Servlet < WEBrick::HTTPServlet::AbstractServlet - def initialize(this, chunked = false) - @chunked = chunked - end - - def do_GET(req, res) - res['Content-Type'] = $test_net_http_data_type - res.body = $test_net_http_data - res.chunked = @chunked - end + def self.clean_http_proxy_env + orig = { + 'http_proxy' => ENV['http_proxy'], + 'http_proxy_user' => ENV['http_proxy_user'], + 'http_proxy_pass' => ENV['http_proxy_pass'], + 'no_proxy' => ENV['no_proxy'], + } - # echo server - def do_POST(req, res) - res['Content-Type'] = req['Content-Type'] - res['X-request-uri'] = req.request_uri.to_s - res.body = req.body - res.chunked = @chunked + orig.each_key do |key| + ENV.delete key end - def do_PATCH(req, res) - res['Content-Type'] = req['Content-Type'] - res.body = req.body - res.chunked = @chunked + yield + ensure + orig.each do |key, value| + ENV[key] = value end end - - class NullWriter - def <<(s) end - def puts(*args) end - def print(*args) end - def printf(*args) end - end end |
