diff options
Diffstat (limited to 'test/net/http')
| -rw-r--r-- | test/net/http/test_http.rb | 107 | ||||
| -rw-r--r-- | test/net/http/test_http_request.rb | 34 | ||||
| -rw-r--r-- | test/net/http/test_httpresponse.rb | 39 | ||||
| -rw-r--r-- | test/net/http/test_https.rb | 139 | ||||
| -rw-r--r-- | test/net/http/test_https_proxy.rb | 51 | ||||
| -rw-r--r-- | test/net/http/utils.rb | 364 |
6 files changed, 568 insertions, 166 deletions
diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 0508645ac5..4e7fa22756 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -126,10 +126,10 @@ class TestNetHTTP < Test::Unit::TestCase def test_proxy_address_no_proxy TestNetHTTPUtils.clean_http_proxy_env do - http = Net::HTTP.new 'hostname.example', nil, 'proxy.example', nil, nil, nil, 'example' + 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.example', nil, nil, nil, 'example,10.224.0.0/22' + 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 @@ -254,6 +254,18 @@ class TestNetHTTP < Test::Unit::TestCase 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 @@ -442,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 @@ -478,12 +494,10 @@ module TestNetHTTP_version_1_1_methods def test_s_post url = "http://#{config('host')}:#{config('port')}/?q=a" - res = assert_warning(/Content-Type did not set/) do - Net::HTTP.post( - URI.parse(url), - "a=x") - end - assert_equal "application/x-www-form-urlencoded", res["Content-Type"] + res = Net::HTTP.post( + 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"] @@ -549,14 +563,12 @@ module TestNetHTTP_version_1_1_methods 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(0.1) + conn.open_timeout = EnvUtil.apply_timeout_scale(1) th = Thread.new do err = !windows? ? Net::WriteTimeout : Net::ReadTimeout assert_raise(err) do - assert_warning(/Content-Type did not set/) do - conn.post('/', "a"*50_000_000) - end + conn.post('/', "a"*50_000_000) end end assert th.join(EnvUtil.apply_timeout_scale(10)) @@ -573,9 +585,9 @@ module TestNetHTTP_version_1_1_methods port = server.addr[1] conn = Net::HTTP.new('localhost', port) - conn.write_timeout = 0.01 - conn.read_timeout = 0.01 if windows? - conn.open_timeout = 0.1 + 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 @@ -629,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) @@ -843,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) } } @@ -887,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') @@ -902,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 @@ -984,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 @@ -1039,7 +1061,7 @@ 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-type' => 'application/x-www-form-urlencoded', 'content-length' => '5', 'expect' => '100-continue'} @@ -1084,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 @@ -1159,11 +1181,11 @@ class TestNetHTTPKeepAlive < Test::Unit::TestCase end def test_keep_alive_reset_on_new_connection - # Using WEBrick's debug log output on accepting 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 - @server.logger.level = WEBrick::BasicLog::DEBUG + @logger_level = :debug start {|http| res = http.get('/') @@ -1234,6 +1256,16 @@ class TestNetHTTPKeepAlive < Test::Unit::TestCase } 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 @@ -1368,3 +1400,28 @@ class TestNetHTTPPartialResponse < Test::Unit::TestCase 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 7fd82b0353..9f5cf4f8f5 100644 --- a/test/net/http/test_http_request.rb +++ b/test/net/http/test_http_request.rb @@ -74,6 +74,18 @@ class HTTPRequestTest < Test::Unit::TestCase 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 @@ -89,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_httpresponse.rb b/test/net/http/test_httpresponse.rb index 394b4c5bfa..01281063cd 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -312,8 +312,8 @@ EOS end def test_read_body_block_mod - # http://ci.rvm.jp/results/trunk-mjit-wait@silicon-docker/3019353 - if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? + # 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| @@ -589,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' diff --git a/test/net/http/test_https.rb b/test/net/http/test_https.rb index 72a69af1a5..f5b21b901f 100644 --- a/test/net/http/test_https.rb +++ b/test/net/http/test_https.rb @@ -7,6 +7,8 @@ rescue LoadError # should skip this test end +return unless defined?(OpenSSL::SSL) + class TestNetHTTPS < Test::Unit::TestCase include TestNetHTTPUtils @@ -19,7 +21,6 @@ class TestNetHTTPS < Test::Unit::TestCase 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 = { @@ -29,25 +30,16 @@ class TestNetHTTPS < Test::Unit::TestCase '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(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) } - # TODO: OpenSSL 1.1.1h seems to yield only SERVER_CERT; need to check the incompatibility - certs.zip([CA_CERT, SERVER_CERT][-certs.size..-1]) do |actual, expected| - assert_equal(expected.to_der, actual.to_der) - end end def test_get_SNI @@ -55,18 +47,10 @@ class TestNetHTTPS < Test::Unit::TestCase 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) + assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) } - # TODO: OpenSSL 1.1.1h seems to yield only SERVER_CERT; need to check the incompatibility - certs.zip([CA_CERT, SERVER_CERT][-certs.size..-1]) do |actual, expected| - assert_equal(expected.to_der, actual.to_der) - end end def test_get_SNI_proxy @@ -78,11 +62,6 @@ class TestNetHTTPS < Test::Unit::TestCase 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 @@ -114,11 +93,6 @@ class TestNetHTTPS < Test::Unit::TestCase 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 @@ -135,10 +109,6 @@ class TestNetHTTPS < Test::Unit::TestCase 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. - omit if OpenSSL::OPENSSL_LIBRARY_VERSION.include?('OpenSSL 1.1.0h') - http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true http.cert_store = TEST_STORE @@ -148,7 +118,7 @@ class TestNetHTTPS < Test::Unit::TestCase # 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 + http.ssl_version = :TLSv1_2 end http.start @@ -165,23 +135,20 @@ class TestNetHTTPS < Test::Unit::TestCase end def test_session_reuse_but_expire - # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h. - omit if OpenSSL::OPENSSL_LIBRARY_VERSION.include?('OpenSSL 1.1.0h') - 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 - + sleep 1.25 http.start http.get("/") socket = http.instance_variable_get(:@socket).io - assert_equal false, socket.session_reused? + assert_equal false, socket.session_reused?, "NOTE: OpenSSL library version is #{OpenSSL::OPENSSL_LIBRARY_VERSION}" http.finish end @@ -238,27 +205,21 @@ class TestNetHTTPS < Test::Unit::TestCase 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 - # the certificate's subject has CN=localhost - http = Net::HTTP.new(HOST_IP, config("port")) + def test_verify_callback + http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true http.cert_store = TEST_STORE - @log_tester = lambda {|_| } - ex = assert_raise(OpenSSL::SSL::SSLError){ - http.request_get("/") {|res| } + certs = [] + http.verify_callback = Proc.new {|preverify_ok, store_ctx| + certs << store_ctx.current_cert + preverify_ok } - re_msg = /certificate verify failed|hostname \"#{HOST_IP}\" does not match/ - assert_match(re_msg, 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 @@ -293,12 +254,10 @@ class TestNetHTTPS < Test::Unit::TestCase end def test_max_version - http = Net::HTTP.new(HOST_IP, config("port")) + http = Net::HTTP.new(HOST, config("port")) http.use_ssl = true http.max_version = :SSL2 - http.verify_callback = Proc.new do |preverify_ok, store_ctx| - true - end + http.cert_store = TEST_STORE @log_tester = lambda {|_| } ex = assert_raise(OpenSSL::SSL::SSLError){ http.request_get("/") {|res| } @@ -307,4 +266,60 @@ class TestNetHTTPS < Test::Unit::TestCase assert_match(re_msg, ex.message) end -end if defined?(OpenSSL::SSL) + 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 4c2a92ccd6..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 - omit '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 e343e16712..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,91 +254,96 @@ 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] - end - - $test_net_http = nil - $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) - if req['Accept'] != '*/*' - res['Content-Type'] = req['Accept'] + @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 - res['Content-Type'] = $test_net_http_data_type + socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" end - res.body = $test_net_http_data - res.chunked = @chunked end + @server.start + @config['port'] = @server.port + end - # 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 + 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 do_PATCH(req, res) - res['Content-Type'] = req['Content-Type'] - res.body = req.body - res.chunked = @chunked + 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 - class NullWriter - def <<(s) end - def puts(*args) end - def print(*args) end - def printf(*args) 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.force_encoding("ASCII-8BIT") + $test_net_http_data_type = 'application/octet-stream' + def self.clean_http_proxy_env orig = { - 'http_proxy' => ENV['http_proxy'], + 'http_proxy' => ENV['http_proxy'], 'http_proxy_user' => ENV['http_proxy_user'], 'http_proxy_pass' => ENV['http_proxy_pass'], - 'no_proxy' => ENV['no_proxy'], + 'no_proxy' => ENV['no_proxy'], } orig.each_key do |key| |
