diff options
Diffstat (limited to 'tool/test/webrick')
24 files changed, 3804 insertions, 0 deletions
diff --git a/tool/test/webrick/.htaccess b/tool/test/webrick/.htaccess new file mode 100644 index 0000000000..69d4659b9f --- /dev/null +++ b/tool/test/webrick/.htaccess @@ -0,0 +1 @@ +this file should not be published. diff --git a/tool/test/webrick/test_cgi.rb b/tool/test/webrick/test_cgi.rb new file mode 100644 index 0000000000..7a75cf565e --- /dev/null +++ b/tool/test/webrick/test_cgi.rb @@ -0,0 +1,170 @@ +# coding: US-ASCII +# frozen_string_literal: false +require_relative "utils" +require "webrick" +require "test/unit" + +class TestWEBrickCGI < Test::Unit::TestCase + CRLF = "\r\n" + + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def start_cgi_server(log_tester=TestWEBrick::DefaultLogTester, &block) + config = { + :CGIInterpreter => TestWEBrick::RubyBin, + :DocumentRoot => File.dirname(__FILE__), + :DirectoryIndex => ["webrick.cgi"], + :RequestCallback => Proc.new{|req, res| + def req.meta_vars + meta = super + meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) + meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] + return meta + end + }, + } + if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ + config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. + end + TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| + block.call(server, addr, port, log) + } + end + + def test_cgi + start_cgi_server{|server, addr, port, log| + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/webrick.cgi") + http.request(req){|res| assert_equal("/webrick.cgi", res.body, log.call)} + req = Net::HTTP::Get.new("/webrick.cgi/path/info") + http.request(req){|res| assert_equal("/path/info", res.body, log.call)} + req = Net::HTTP::Get.new("/webrick.cgi/%3F%3F%3F?foo=bar") + http.request(req){|res| assert_equal("/???", res.body, log.call)} + unless RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32|java/ + # Path info of res.body is passed via ENV. + # ENV[] returns different value on Windows depending on locale. + req = Net::HTTP::Get.new("/webrick.cgi/%A4%DB%A4%B2/%A4%DB%A4%B2") + http.request(req){|res| + assert_equal("/\xA4\xDB\xA4\xB2/\xA4\xDB\xA4\xB2", res.body, log.call)} + end + req = Net::HTTP::Get.new("/webrick.cgi?a=1;a=2;b=x") + http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} + req = Net::HTTP::Get.new("/webrick.cgi?a=1&a=2&b=x") + http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} + + req = Net::HTTP::Post.new("/webrick.cgi?a=x;a=y;b=1") + req["Content-Type"] = "application/x-www-form-urlencoded" + http.request(req, "a=1;a=2;b=x"){|res| + assert_equal("a=1, a=2, b=x", res.body, log.call)} + req = Net::HTTP::Post.new("/webrick.cgi?a=x&a=y&b=1") + req["Content-Type"] = "application/x-www-form-urlencoded" + http.request(req, "a=1&a=2&b=x"){|res| + assert_equal("a=1, a=2, b=x", res.body, log.call)} + req = Net::HTTP::Get.new("/") + http.request(req){|res| + ary = res.body.lines.to_a + assert_match(%r{/$}, ary[0], log.call) + assert_match(%r{/webrick.cgi$}, ary[1], log.call) + } + + req = Net::HTTP::Get.new("/webrick.cgi") + req["Cookie"] = "CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001" + http.request(req){|res| + assert_equal( + "CUSTOMER=WILE_E_COYOTE\nPART_NUMBER=ROCKET_LAUNCHER_0001\n", + res.body, log.call) + } + + req = Net::HTTP::Get.new("/webrick.cgi") + cookie = %{$Version="1"; } + cookie << %{Customer="WILE_E_COYOTE"; $Path="/acme"; } + cookie << %{Part_Number="Rocket_Launcher_0001"; $Path="/acme"; } + cookie << %{Shipping="FedEx"; $Path="/acme"} + req["Cookie"] = cookie + http.request(req){|res| + assert_equal("Customer=WILE_E_COYOTE, Shipping=FedEx", + res["Set-Cookie"], log.call) + assert_equal("Customer=WILE_E_COYOTE\n" + + "Part_Number=Rocket_Launcher_0001\n" + + "Shipping=FedEx\n", res.body, log.call) + } + } + end + + def test_bad_request + log_tester = lambda {|log, access_log| + assert_match(/BadRequest/, log.join) + } + start_cgi_server(log_tester) {|server, addr, port, log| + sock = TCPSocket.new(addr, port) + begin + sock << "POST /webrick.cgi HTTP/1.0" << CRLF + sock << "Content-Type: application/x-www-form-urlencoded" << CRLF + sock << "Content-Length: 1024" << CRLF + sock << CRLF + sock << "a=1&a=2&b=x" + sock.close_write + assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, sock.read, log.call) + ensure + sock.close + end + } + end + + def test_cgi_env + start_cgi_server do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/webrick.cgi/dumpenv") + req['proxy'] = 'http://example.com/' + req['hello'] = 'world' + http.request(req) do |res| + env = Marshal.load(res.body) + assert_equal 'world', env['HTTP_HELLO'] + assert_not_operator env, :include?, 'HTTP_PROXY' + end + end + end + + CtrlSeq = [0x7f, *(1..31)].pack("C*").gsub(/\s+/, '') + CtrlPat = /#{Regexp.quote(CtrlSeq)}/o + DumpPat = /#{Regexp.quote(CtrlSeq.dump[1...-1])}/o + + def test_bad_uri + log_tester = lambda {|log, access_log| + assert_equal(1, log.length) + assert_match(/ERROR bad URI/, log[0]) + } + start_cgi_server(log_tester) {|server, addr, port, log| + res = TCPSocket.open(addr, port) {|sock| + sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}" + sock.close_write + sock.read + } + assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) + s = log.call.each_line.grep(/ERROR bad URI/)[0] + assert_match(DumpPat, s) + assert_not_match(CtrlPat, s) + } + end + + def test_bad_header + log_tester = lambda {|log, access_log| + assert_equal(1, log.length) + assert_match(/ERROR bad header/, log[0]) + } + start_cgi_server(log_tester) {|server, addr, port, log| + res = TCPSocket.open(addr, port) {|sock| + sock << "GET / HTTP/1.0#{CRLF}#{CtrlSeq}#{CRLF}#{CRLF}" + sock.close_write + sock.read + } + assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) + s = log.call.each_line.grep(/ERROR bad header/)[0] + assert_match(DumpPat, s) + assert_not_match(CtrlPat, s) + } + end +end diff --git a/tool/test/webrick/test_config.rb b/tool/test/webrick/test_config.rb new file mode 100644 index 0000000000..a54a667452 --- /dev/null +++ b/tool/test/webrick/test_config.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/config" + +class TestWEBrickConfig < Test::Unit::TestCase + def test_server_name_default + config = WEBrick::Config::General.dup + assert_equal(false, config.key?(:ServerName)) + assert_equal(WEBrick::Utils.getservername, config[:ServerName]) + assert_equal(true, config.key?(:ServerName)) + end + + def test_server_name_set_nil + config = WEBrick::Config::General.dup.update(ServerName: nil) + assert_equal(nil, config[:ServerName]) + end +end diff --git a/tool/test/webrick/test_cookie.rb b/tool/test/webrick/test_cookie.rb new file mode 100644 index 0000000000..e46185f127 --- /dev/null +++ b/tool/test/webrick/test_cookie.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/cookie" + +class TestWEBrickCookie < Test::Unit::TestCase + def test_new + cookie = WEBrick::Cookie.new("foo","bar") + assert_equal("foo", cookie.name) + assert_equal("bar", cookie.value) + assert_equal("foo=bar", cookie.to_s) + end + + def test_time + cookie = WEBrick::Cookie.new("foo","bar") + t = 1000000000 + cookie.max_age = t + assert_match(t.to_s, cookie.to_s) + + cookie = WEBrick::Cookie.new("foo","bar") + t = Time.at(1000000000) + cookie.expires = t + assert_equal(Time, cookie.expires.class) + assert_equal(t, cookie.expires) + ts = t.httpdate + cookie.expires = ts + assert_equal(Time, cookie.expires.class) + assert_equal(t, cookie.expires) + assert_match(ts, cookie.to_s) + end + + def test_parse + data = "" + data << '$Version="1"; ' + data << 'Customer="WILE_E_COYOTE"; $Path="/acme"; ' + data << 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"; ' + data << 'Shipping="FedEx"; $Path="/acme"' + cookies = WEBrick::Cookie.parse(data) + assert_equal(3, cookies.size) + assert_equal(1, cookies[0].version) + assert_equal("Customer", cookies[0].name) + assert_equal("WILE_E_COYOTE", cookies[0].value) + assert_equal("/acme", cookies[0].path) + assert_equal(1, cookies[1].version) + assert_equal("Part_Number", cookies[1].name) + assert_equal("Rocket_Launcher_0001", cookies[1].value) + assert_equal(1, cookies[2].version) + assert_equal("Shipping", cookies[2].name) + assert_equal("FedEx", cookies[2].value) + + data = "hoge=moge; __div__session=9865ecfd514be7f7" + cookies = WEBrick::Cookie.parse(data) + assert_equal(2, cookies.size) + assert_equal(0, cookies[0].version) + assert_equal("hoge", cookies[0].name) + assert_equal("moge", cookies[0].value) + assert_equal("__div__session", cookies[1].name) + assert_equal("9865ecfd514be7f7", cookies[1].value) + + # don't allow ,-separator + data = "hoge=moge, __div__session=9865ecfd514be7f7" + cookies = WEBrick::Cookie.parse(data) + assert_equal(1, cookies.size) + assert_equal(0, cookies[0].version) + assert_equal("hoge", cookies[0].name) + assert_equal("moge, __div__session=9865ecfd514be7f7", cookies[0].value) + end + + def test_parse_no_whitespace + data = [ + '$Version="1"; ', + 'Customer="WILE_E_COYOTE";$Path="/acme";', # no SP between cookie-string + 'Part_Number="Rocket_Launcher_0001";$Path="/acme";', # no SP between cookie-string + 'Shipping="FedEx";$Path="/acme"' + ].join + cookies = WEBrick::Cookie.parse(data) + assert_equal(1, cookies.size) + end + + def test_parse_too_much_whitespaces + # According to RFC6265, + # cookie-string = cookie-pair *( ";" SP cookie-pair ) + # So single 0x20 is needed after ';'. We allow multiple spaces here for + # compatibility with older WEBrick versions. + data = [ + '$Version="1"; ', + 'Customer="WILE_E_COYOTE";$Path="/acme"; ', # no SP between cookie-string + 'Part_Number="Rocket_Launcher_0001";$Path="/acme"; ', # no SP between cookie-string + 'Shipping="FedEx";$Path="/acme"' + ].join + cookies = WEBrick::Cookie.parse(data) + assert_equal(3, cookies.size) + end + + def test_parse_set_cookie + data = %(Customer="WILE_E_COYOTE"; Version="1"; Path="/acme") + cookie = WEBrick::Cookie.parse_set_cookie(data) + assert_equal("Customer", cookie.name) + assert_equal("WILE_E_COYOTE", cookie.value) + assert_equal(1, cookie.version) + assert_equal("/acme", cookie.path) + + data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) + cookie = WEBrick::Cookie.parse_set_cookie(data) + assert_equal("Shipping", cookie.name) + assert_equal("FedEx", cookie.value) + assert_equal(1, cookie.version) + assert_equal("/acme", cookie.path) + assert_equal(true, cookie.secure) + end + + def test_parse_set_cookies + data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) + data << %(, CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT; path=/; Secure) + data << %(, name="Aaron"; Version="1"; path="/acme") + cookies = WEBrick::Cookie.parse_set_cookies(data) + assert_equal(3, cookies.length) + + fed_ex = cookies.find { |c| c.name == 'Shipping' } + assert_not_nil(fed_ex) + assert_equal("Shipping", fed_ex.name) + assert_equal("FedEx", fed_ex.value) + assert_equal(1, fed_ex.version) + assert_equal("/acme", fed_ex.path) + assert_equal(true, fed_ex.secure) + + name = cookies.find { |c| c.name == 'name' } + assert_not_nil(name) + assert_equal("name", name.name) + assert_equal("Aaron", name.value) + assert_equal(1, name.version) + assert_equal("/acme", name.path) + + customer = cookies.find { |c| c.name == 'CUSTOMER' } + assert_not_nil(customer) + assert_equal("CUSTOMER", customer.name) + assert_equal("WILE_E_COYOTE", customer.value) + assert_equal(0, customer.version) + assert_equal("/", customer.path) + assert_equal(Time.utc(1999, 11, 9, 23, 12, 40), customer.expires) + end +end diff --git a/tool/test/webrick/test_do_not_reverse_lookup.rb b/tool/test/webrick/test_do_not_reverse_lookup.rb new file mode 100644 index 0000000000..efcb5a9299 --- /dev/null +++ b/tool/test/webrick/test_do_not_reverse_lookup.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick" +require_relative "utils" + +class TestDoNotReverseLookup < Test::Unit::TestCase + class DNRL < WEBrick::GenericServer + def run(sock) + sock << sock.do_not_reverse_lookup.to_s + end + end + + @@original_do_not_reverse_lookup_value = Socket.do_not_reverse_lookup + + def teardown + Socket.do_not_reverse_lookup = @@original_do_not_reverse_lookup_value + end + + def do_not_reverse_lookup?(config) + result = nil + TestWEBrick.start_server(DNRL, config) do |server, addr, port, log| + TCPSocket.open(addr, port) do |sock| + result = {'true' => true, 'false' => false}[sock.gets] + end + end + result + end + + # +--------------------------------------------------------------------------+ + # | Expected interaction between Socket.do_not_reverse_lookup | + # | and WEBrick::Config::General[:DoNotReverseLookup] | + # +----------------------------+---------------------------------------------+ + # | |WEBrick::Config::General[:DoNotReverseLookup]| + # +----------------------------+--------------+---------------+--------------+ + # |Socket.do_not_reverse_lookup| TRUE | FALSE | NIL | + # +----------------------------+--------------+---------------+--------------+ + # | TRUE | true | false | true | + # +----------------------------+--------------+---------------+--------------+ + # | FALSE | true | false | false | + # +----------------------------+--------------+---------------+--------------+ + + def test_socket_dnrl_true_server_dnrl_true + Socket.do_not_reverse_lookup = true + assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) + end + + def test_socket_dnrl_true_server_dnrl_false + Socket.do_not_reverse_lookup = true + assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) + end + + def test_socket_dnrl_true_server_dnrl_nil + Socket.do_not_reverse_lookup = true + assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) + end + + def test_socket_dnrl_false_server_dnrl_true + Socket.do_not_reverse_lookup = false + assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) + end + + def test_socket_dnrl_false_server_dnrl_false + Socket.do_not_reverse_lookup = false + assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) + end + + def test_socket_dnrl_false_server_dnrl_nil + Socket.do_not_reverse_lookup = false + assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) + end +end diff --git a/tool/test/webrick/test_filehandler.rb b/tool/test/webrick/test_filehandler.rb new file mode 100644 index 0000000000..998e03f690 --- /dev/null +++ b/tool/test/webrick/test_filehandler.rb @@ -0,0 +1,402 @@ +# frozen_string_literal: false +require "test/unit" +require_relative "utils.rb" +require "webrick" +require "stringio" +require "tmpdir" + +class WEBrick::TestFileHandler < Test::Unit::TestCase + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def default_file_handler(filename) + klass = WEBrick::HTTPServlet::DefaultFileHandler + klass.new(WEBrick::Config::HTTP, filename) + end + + def windows? + File.directory?("\\") + end + + def get_res_body(res) + sio = StringIO.new + sio.binmode + res.send_body(sio) + sio.string + end + + def make_range_request(range_spec) + msg = <<-END_OF_REQUEST + GET / HTTP/1.0 + Range: #{range_spec} + + END_OF_REQUEST + return StringIO.new(msg.gsub(/^ {6}/, "")) + end + + def make_range_response(file, range_spec) + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(make_range_request(range_spec)) + res = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP) + size = File.size(file) + handler = default_file_handler(file) + handler.make_partial_content(req, res, file, size) + return res + end + + def test_make_partial_content + filename = __FILE__ + filesize = File.size(filename) + + res = make_range_response(filename, "bytes=#{filesize-100}-") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(100, get_res_body(res).size) + + res = make_range_response(filename, "bytes=-100") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(100, get_res_body(res).size) + + res = make_range_response(filename, "bytes=0-99") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(100, get_res_body(res).size) + + res = make_range_response(filename, "bytes=100-199") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(100, get_res_body(res).size) + + res = make_range_response(filename, "bytes=0-0") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(1, get_res_body(res).size) + + res = make_range_response(filename, "bytes=-1") + assert_match(%r{^text/plain}, res["content-type"]) + assert_equal(1, get_res_body(res).size) + + res = make_range_response(filename, "bytes=0-0, -2") + assert_match(%r{^multipart/byteranges}, res["content-type"]) + body = get_res_body(res) + boundary = /; boundary=(.+)/.match(res['content-type'])[1] + off = filesize - 2 + last = filesize - 1 + + exp = "--#{boundary}\r\n" \ + "Content-Type: text/plain\r\n" \ + "Content-Range: bytes 0-0/#{filesize}\r\n" \ + "\r\n" \ + "#{IO.read(__FILE__, 1)}\r\n" \ + "--#{boundary}\r\n" \ + "Content-Type: text/plain\r\n" \ + "Content-Range: bytes #{off}-#{last}/#{filesize}\r\n" \ + "\r\n" \ + "#{IO.read(__FILE__, 2, off)}\r\n" \ + "--#{boundary}--\r\n" + assert_equal exp, body + end + + def test_filehandler + config = { :DocumentRoot => File.dirname(__FILE__), } + this_file = File.basename(__FILE__) + filesize = File.size(__FILE__) + this_data = File.binread(__FILE__) + range = nil + bug2593 = '[ruby-dev:40030]' + + TestWEBrick.start_httpserver(config) do |server, addr, port, log| + begin + server[:DocumentRootOptions][:NondisclosureName] = [] + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/") + http.request(req){|res| + assert_equal("200", res.code, log.call) + assert_equal("text/html", res.content_type, log.call) + assert_match(/HREF="#{this_file}"/, res.body, log.call) + } + req = Net::HTTP::Get.new("/#{this_file}") + http.request(req){|res| + assert_equal("200", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_equal(this_data, res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=#{filesize-100}-") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal((filesize-100)..(filesize-1), range, log.call) + assert_equal(this_data[-100..-1], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-100") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal((filesize-100)..(filesize-1), range, log.call) + assert_equal(this_data[-100..-1], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-99") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal(0..99, range, log.call) + assert_equal(this_data[0..99], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=100-199") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal(100..199, range, log.call) + assert_equal(this_data[100..199], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal(0..0, range, log.call) + assert_equal(this_data[0..0], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-1") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("text/plain", res.content_type, log.call) + assert_nothing_raised(bug2593) {range = res.content_range} + assert_equal((filesize-1)..(filesize-1), range, log.call) + assert_equal(this_data[-1, 1], res.body, log.call) + } + + req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0, -2") + http.request(req){|res| + assert_equal("206", res.code, log.call) + assert_equal("multipart/byteranges", res.content_type, log.call) + } + ensure + server[:DocumentRootOptions].delete :NondisclosureName + end + end + end + + def test_non_disclosure_name + config = { :DocumentRoot => File.dirname(__FILE__), } + log_tester = lambda {|log, access_log| + log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } + log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s } + assert_equal([], log) + } + this_file = File.basename(__FILE__) + TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + doc_root_opts = server[:DocumentRootOptions] + doc_root_opts[:NondisclosureName] = %w(.ht* *~ test_*) + req = Net::HTTP::Get.new("/") + http.request(req){|res| + assert_equal("200", res.code, log.call) + assert_equal("text/html", res.content_type, log.call) + assert_no_match(/HREF="#{File.basename(__FILE__)}"/, res.body) + } + req = Net::HTTP::Get.new("/#{this_file}") + http.request(req){|res| + assert_equal("404", res.code, log.call) + } + doc_root_opts[:NondisclosureName] = %w(.ht* *~ TEST_*) + http.request(req){|res| + assert_equal("404", res.code, log.call) + } + end + end + + def test_directory_traversal + return if File.executable?(__FILE__) # skip on strange file system + + config = { :DocumentRoot => File.dirname(__FILE__), } + log_tester = lambda {|log, access_log| + log = log.reject {|s| /ERROR bad URI/ =~ s } + log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } + assert_equal([], log) + } + TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/../../") + http.request(req){|res| assert_equal("400", res.code, log.call) } + req = Net::HTTP::Get.new("/..%5c../#{File.basename(__FILE__)}") + http.request(req){|res| assert_equal(windows? ? "200" : "404", res.code, log.call) } + req = Net::HTTP::Get.new("/..%5c..%5cruby.c") + http.request(req){|res| assert_equal("404", res.code, log.call) } + end + end + + def test_unwise_in_path + if windows? + config = { :DocumentRoot => File.dirname(__FILE__), } + TestWEBrick.start_httpserver(config) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/..%5c..") + http.request(req){|res| assert_equal("301", res.code, log.call) } + end + end + end + + def test_short_filename + return if File.executable?(__FILE__) # skip on strange file system + + config = { + :CGIInterpreter => TestWEBrick::RubyBin, + :DocumentRoot => File.dirname(__FILE__), + :CGIPathEnv => ENV['PATH'], + } + log_tester = lambda {|log, access_log| + log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } + log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s } + assert_equal([], log) + } + TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + if windows? + root = config[:DocumentRoot].tr("/", "\\") + fname = IO.popen(%W[dir /x #{root}\\webrick_long_filename.cgi], encoding: "binary", &:read) + fname.sub!(/\A.*$^$.*$^$/m, '') + if fname + fname = fname[/\s(w.+?cgi)\s/i, 1] + fname.downcase! + end + else + fname = "webric~1.cgi" + end + req = Net::HTTP::Get.new("/#{fname}/test") + http.request(req) do |res| + if windows? + assert_equal("200", res.code, log.call) + assert_equal("/test", res.body, log.call) + else + assert_equal("404", res.code, log.call) + end + end + + req = Net::HTTP::Get.new("/.htaccess") + http.request(req) {|res| assert_equal("404", res.code, log.call) } + req = Net::HTTP::Get.new("/htacce~1") + http.request(req) {|res| assert_equal("404", res.code, log.call) } + req = Net::HTTP::Get.new("/HTACCE~1") + http.request(req) {|res| assert_equal("404", res.code, log.call) } + end + end + + def test_multibyte_char_in_path + if Encoding.default_external == Encoding.find('US-ASCII') + reset_encoding = true + verb = $VERBOSE + $VERBOSE = false + Encoding.default_external = Encoding.find('UTF-8') + end + + c = "\u00a7" + begin + c = c.encode('filesystem') + rescue EncodingError + c = c.b + end + Dir.mktmpdir(c) do |dir| + basename = "#{c}.txt" + File.write("#{dir}/#{basename}", "test_multibyte_char_in_path") + Dir.mkdir("#{dir}/#{c}") + File.write("#{dir}/#{c}/#{basename}", "nested") + config = { + :DocumentRoot => dir, + :DirectoryIndex => [basename], + } + TestWEBrick.start_httpserver(config) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + path = "/#{basename}" + req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path)) + http.request(req){|res| assert_equal("200", res.code, log.call + "\nFilesystem encoding is #{Encoding.find('filesystem')}") } + path = "/#{c}/#{basename}" + req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path)) + http.request(req){|res| assert_equal("200", res.code, log.call) } + req = Net::HTTP::Get.new('/') + http.request(req){|res| + assert_equal("test_multibyte_char_in_path", res.body, log.call) + } + end + end + ensure + if reset_encoding + Encoding.default_external = Encoding.find('US-ASCII') + $VERBOSE = verb + end + end + + def test_script_disclosure + return if File.executable?(__FILE__) # skip on strange file system + + config = { + :CGIInterpreter => TestWEBrick::RubyBinArray, + :DocumentRoot => File.dirname(__FILE__), + :CGIPathEnv => ENV['PATH'], + :RequestCallback => Proc.new{|req, res| + def req.meta_vars + meta = super + meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) + meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] + return meta + end + }, + } + log_tester = lambda {|log, access_log| + log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } + assert_equal([], log) + } + TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + http.read_timeout = EnvUtil.apply_timeout_scale(60) + http.write_timeout = EnvUtil.apply_timeout_scale(60) if http.respond_to?(:write_timeout=) + + req = Net::HTTP::Get.new("/webrick.cgi/test") + http.request(req) do |res| + assert_equal("200", res.code, log.call) + assert_equal("/test", res.body, log.call) + end + + resok = windows? + response_assertion = Proc.new do |res| + if resok + assert_equal("200", res.code, log.call) + assert_equal("/test", res.body, log.call) + else + assert_equal("404", res.code, log.call) + end + end + req = Net::HTTP::Get.new("/webrick.cgi%20/test") + http.request(req, &response_assertion) + req = Net::HTTP::Get.new("/webrick.cgi./test") + http.request(req, &response_assertion) + resok &&= File.exist?(__FILE__+"::$DATA") + req = Net::HTTP::Get.new("/webrick.cgi::$DATA/test") + http.request(req, &response_assertion) + end + end + + def test_erbhandler + config = { :DocumentRoot => File.dirname(__FILE__) } + log_tester = lambda {|log, access_log| + log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } + assert_equal([], log) + } + TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/webrick.rhtml") + http.request(req) do |res| + assert_equal("200", res.code, log.call) + assert_match %r!\Areq to http://[^/]+/webrick\.rhtml {}\n!, res.body + end + end + end +end diff --git a/tool/test/webrick/test_htgroup.rb b/tool/test/webrick/test_htgroup.rb new file mode 100644 index 0000000000..8749711df5 --- /dev/null +++ b/tool/test/webrick/test_htgroup.rb @@ -0,0 +1,19 @@ +require "tempfile" +require "test/unit" +require "webrick/httpauth/htgroup" + +class TestHtgroup < Test::Unit::TestCase + def test_htgroup + Tempfile.create('test_htgroup') do |tmpfile| + tmpfile.close + tmp_group = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) + tmp_group.add 'superheroes', %w[spiderman batman] + tmp_group.add 'supervillains', %w[joker] + tmp_group.flush + + htgroup = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) + assert_equal(htgroup.members('superheroes'), %w[spiderman batman]) + assert_equal(htgroup.members('supervillains'), %w[joker]) + end + end +end diff --git a/tool/test/webrick/test_htmlutils.rb b/tool/test/webrick/test_htmlutils.rb new file mode 100644 index 0000000000..ae1b8efa95 --- /dev/null +++ b/tool/test/webrick/test_htmlutils.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/htmlutils" + +class TestWEBrickHTMLUtils < Test::Unit::TestCase + include WEBrick::HTMLUtils + + def test_escape + assert_equal("foo", escape("foo")) + assert_equal("foo bar", escape("foo bar")) + assert_equal("foo&bar", escape("foo&bar")) + assert_equal("foo"bar", escape("foo\"bar")) + assert_equal("foo>bar", escape("foo>bar")) + assert_equal("foo<bar", escape("foo<bar")) + assert_equal("\u{3053 3093 306B 3061 306F}", escape("\u{3053 3093 306B 3061 306F}")) + bug8425 = '[Bug #8425] [ruby-core:55052]' + assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) { + assert_equal("\u{3053 3093 306B}\xff<", escape("\u{3053 3093 306B}\xff<")) + } + end +end diff --git a/tool/test/webrick/test_httpauth.rb b/tool/test/webrick/test_httpauth.rb new file mode 100644 index 0000000000..9fe8af8be2 --- /dev/null +++ b/tool/test/webrick/test_httpauth.rb @@ -0,0 +1,366 @@ +# frozen_string_literal: false +require "test/unit" +require "net/http" +require "tempfile" +require "webrick" +require "webrick/httpauth/basicauth" +require "stringio" +require_relative "utils" + +class TestWEBrickHTTPAuth < Test::Unit::TestCase + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def test_basic_auth + log_tester = lambda {|log, access_log| + assert_equal(1, log.length) + assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[0]) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/basic_auth" + + server.mount_proc(path){|req, res| + WEBrick::HTTPAuth.basic_auth(req, res, realm){|user, pass| + user == "webrick" && pass == "supersecretpassword" + } + res.body = "hoge" + } + http = Net::HTTP.new(addr, port) + g = Net::HTTP::Get.new(path) + g.basic_auth("webrick", "supersecretpassword") + http.request(g){|res| assert_equal("hoge", res.body, log.call)} + g.basic_auth("webrick", "not super") + http.request(g){|res| assert_not_equal("hoge", res.body, log.call)} + } + end + + def test_basic_auth_sha + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.puts("webrick:{SHA}GJYFRpBbdchp595jlh3Bhfmgp8k=") + tmpfile.flush + assert_raise(NotImplementedError){ + WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + } + } + end + + def test_basic_auth_md5 + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.puts("webrick:$apr1$IOVMD/..$rmnOSPXr0.wwrLPZHBQZy0") + tmpfile.flush + assert_raise(NotImplementedError){ + WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path) + } + } + end + + [nil, :crypt, :bcrypt].each do |hash_algo| + # OpenBSD does not support insecure DES-crypt + next if /openbsd/ =~ RUBY_PLATFORM && hash_algo != :bcrypt + + begin + case hash_algo + when :crypt + # require 'string/crypt' + when :bcrypt + require 'bcrypt' + end + rescue LoadError + next + end + + define_method(:"test_basic_auth_htpasswd_#{hash_algo}") do + log_tester = lambda {|log, access_log| + log.reject! {|line| /\A\s*\z/ =~ line } + pats = [ + /ERROR Basic WEBrick's realm: webrick: password unmatch\./, + /ERROR WEBrick::HTTPStatus::Unauthorized/ + ] + pats.each {|pat| + assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}") + log.reject! {|line| pat =~ line } + } + assert_equal([], log) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/basic_auth2" + + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) + tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") + tmp_pass.set_passwd(realm, "foo", "supersecretpassword") + tmp_pass.flush + + htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) + users = [] + htpasswd.each{|user, pass| users << user } + assert_equal(2, users.size, log.call) + assert(users.member?("webrick"), log.call) + assert(users.member?("foo"), log.call) + + server.mount_proc(path){|req, res| + auth = WEBrick::HTTPAuth::BasicAuth.new( + :Realm => realm, :UserDB => htpasswd, + :Logger => server.logger + ) + auth.authenticate(req, res) + res.body = "hoge" + } + http = Net::HTTP.new(addr, port) + g = Net::HTTP::Get.new(path) + g.basic_auth("webrick", "supersecretpassword") + http.request(g){|res| assert_equal("hoge", res.body, log.call)} + g.basic_auth("webrick", "not super") + http.request(g){|res| assert_not_equal("hoge", res.body, log.call)} + } + } + end + + define_method(:"test_basic_auth_bad_username_htpasswd_#{hash_algo}") do + log_tester = lambda {|log, access_log| + assert_equal(2, log.length) + assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed\./, log[0]) + assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1]) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/basic_auth" + + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) + tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") + tmp_pass.set_passwd(realm, "foo", "supersecretpassword") + tmp_pass.flush + + htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) + users = [] + htpasswd.each{|user, pass| users << user } + server.mount_proc(path){|req, res| + auth = WEBrick::HTTPAuth::BasicAuth.new( + :Realm => realm, :UserDB => htpasswd, + :Logger => server.logger + ) + auth.authenticate(req, res) + res.body = "hoge" + } + http = Net::HTTP.new(addr, port) + g = Net::HTTP::Get.new(path) + g.basic_auth("foo\ebar", "passwd") + http.request(g){|res| assert_not_equal("hoge", res.body, log.call) } + } + } + end + end + + DIGESTRES_ = / + ([a-zA-Z\-]+) + [ \t]*(?:\r\n[ \t]*)* + = + [ \t]*(?:\r\n[ \t]*)* + (?: + "((?:[^"]+|\\[\x00-\x7F])*)" | + ([!\#$%&'*+\-.0-9A-Z^_`a-z|~]+) + )/x + + def test_digest_auth + log_tester = lambda {|log, access_log| + log.reject! {|line| /\A\s*\z/ =~ line } + pats = [ + /ERROR Digest WEBrick's realm: no credentials in the request\./, + /ERROR WEBrick::HTTPStatus::Unauthorized/, + /ERROR Digest WEBrick's realm: webrick: digest unmatch\./ + ] + pats.each {|pat| + assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}") + log.reject! {|line| pat =~ line } + } + assert_equal([], log) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "WEBrick's realm" + path = "/digest_auth" + + Tempfile.create("test_webrick_auth") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) + tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") + tmp_pass.set_passwd(realm, "foo", "supersecretpassword") + tmp_pass.flush + + htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) + users = [] + htdigest.each{|user, pass| users << user } + assert_equal(2, users.size, log.call) + assert(users.member?("webrick"), log.call) + assert(users.member?("foo"), log.call) + + auth = WEBrick::HTTPAuth::DigestAuth.new( + :Realm => realm, :UserDB => htdigest, + :Algorithm => 'MD5', + :Logger => server.logger + ) + server.mount_proc(path){|req, res| + auth.authenticate(req, res) + res.body = "hoge" + } + + Net::HTTP.start(addr, port) do |http| + g = Net::HTTP::Get.new(path) + params = {} + http.request(g) do |res| + assert_equal('401', res.code, log.call) + res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token| + params[key.downcase] = token || quoted.delete('\\') + end + params['uri'] = "http://#{addr}:#{port}#{path}" + end + + g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params) + http.request(g){|res| assert_equal("hoge", res.body, log.call)} + + params['algorithm'].downcase! #4936 + g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params) + http.request(g){|res| assert_equal("hoge", res.body, log.call)} + + g['Authorization'] = credentials_for_request('webrick', "not super", params) + http.request(g){|res| assert_not_equal("hoge", res.body, log.call)} + end + } + } + end + + def test_digest_auth_int + log_tester = lambda {|log, access_log| + log.reject! {|line| /\A\s*\z/ =~ line } + pats = [ + /ERROR Digest wb auth-int realm: no credentials in the request\./, + /ERROR WEBrick::HTTPStatus::Unauthorized/, + /ERROR Digest wb auth-int realm: foo: digest unmatch\./ + ] + pats.each {|pat| + assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}") + log.reject! {|line| pat =~ line } + } + assert_equal([], log) + } + TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| + realm = "wb auth-int realm" + path = "/digest_auth_int" + + Tempfile.create("test_webrick_auth_int") {|tmpfile| + tmpfile.close + tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) + tmp_pass.set_passwd(realm, "foo", "Hunter2") + tmp_pass.flush + + htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) + users = [] + htdigest.each{|user, pass| users << user } + assert_equal %w(foo), users + + auth = WEBrick::HTTPAuth::DigestAuth.new( + :Realm => realm, :UserDB => htdigest, + :Algorithm => 'MD5', + :Logger => server.logger, + :Qop => %w(auth-int), + ) + server.mount_proc(path){|req, res| + auth.authenticate(req, res) + res.body = "bbb" + } + Net::HTTP.start(addr, port) do |http| + post = Net::HTTP::Post.new(path) + params = {} + data = 'hello=world' + body = StringIO.new(data) + post.content_length = data.bytesize + post['Content-Type'] = 'application/x-www-form-urlencoded' + post.body_stream = body + + http.request(post) do |res| + assert_equal('401', res.code, log.call) + res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token| + params[key.downcase] = token || quoted.delete('\\') + end + params['uri'] = "http://#{addr}:#{port}#{path}" + end + + body.rewind + cred = credentials_for_request('foo', 'Hunter3', params, body) + post['Authorization'] = cred + post.body_stream = body + http.request(post){|res| + assert_equal('401', res.code, log.call) + assert_not_equal("bbb", res.body, log.call) + } + + body.rewind + cred = credentials_for_request('foo', 'Hunter2', params, body) + post['Authorization'] = cred + post.body_stream = body + http.request(post){|res| assert_equal("bbb", res.body, log.call)} + end + } + } + end + + def test_digest_auth_invalid + digest_auth = WEBrick::HTTPAuth::DigestAuth.new(Realm: 'realm', UserDB: '') + + def digest_auth.error(fmt, *) + end + + def digest_auth.try_bad_request(len) + request = {"Authorization" => %[Digest a="#{'\b'*len}]} + authenticate request, nil + end + + bad_request = WEBrick::HTTPStatus::BadRequest + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + assert_raise(bad_request) {digest_auth.try_bad_request(10)} + limit = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) + [20, 50, 100, 200].each do |len| + assert_raise(bad_request) do + Timeout.timeout(len*limit) {digest_auth.try_bad_request(len)} + end + end + end + + private + def credentials_for_request(user, password, params, body = nil) + cnonce = "hoge" + nonce_count = 1 + ha1 = "#{user}:#{params['realm']}:#{password}" + if body + dig = Digest::MD5.new + while buf = body.read(16384) + dig.update(buf) + end + body.rewind + ha2 = "POST:#{params['uri']}:#{dig.hexdigest}" + else + ha2 = "GET:#{params['uri']}" + end + + request_digest = + "#{Digest::MD5.hexdigest(ha1)}:" \ + "#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \ + "#{Digest::MD5.hexdigest(ha2)}" + "Digest username=\"#{user}\"" \ + ", realm=\"#{params['realm']}\"" \ + ", nonce=\"#{params['nonce']}\"" \ + ", uri=\"#{params['uri']}\"" \ + ", qop=#{params['qop']}" \ + ", nc=#{'%08x' % nonce_count}" \ + ", cnonce=\"#{cnonce}\"" \ + ", response=\"#{Digest::MD5.hexdigest(request_digest)}\"" \ + ", opaque=\"#{params['opaque']}\"" \ + ", algorithm=#{params['algorithm']}" + end +end diff --git a/tool/test/webrick/test_httpproxy.rb b/tool/test/webrick/test_httpproxy.rb new file mode 100644 index 0000000000..1c2f2fce52 --- /dev/null +++ b/tool/test/webrick/test_httpproxy.rb @@ -0,0 +1,466 @@ +# frozen_string_literal: false +require "test/unit" +require "net/http" +require "webrick" +require "webrick/httpproxy" +begin + require "webrick/ssl" + require "net/https" +rescue LoadError + # test_connect will be skipped +end +require File.expand_path("utils.rb", File.dirname(__FILE__)) + +class TestWEBrickHTTPProxy < Test::Unit::TestCase + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def test_fake_proxy + assert_nil(WEBrick::FakeProxyURI.scheme) + assert_nil(WEBrick::FakeProxyURI.host) + assert_nil(WEBrick::FakeProxyURI.port) + assert_nil(WEBrick::FakeProxyURI.path) + assert_nil(WEBrick::FakeProxyURI.userinfo) + assert_raise(NoMethodError){ WEBrick::FakeProxyURI.foo } + end + + def test_proxy + # Testing GET or POST to the proxy server + # Note that the proxy server works as the origin server. + # +------+ + # V | + # client -------> proxy ---+ + # GET / POST GET / POST + # + proxy_handler_called = request_handler_called = 0 + config = { + :ServerName => "localhost.localdomain", + :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, + :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } + } + TestWEBrick.start_httpproxy(config){|server, addr, port, log| + server.mount_proc("/"){|req, res| + res.body = "#{req.request_method} #{req.path} #{req.body}" + } + http = Net::HTTP.new(addr, port, addr, port) + + req = Net::HTTP::Get.new("/") + http.request(req){|res| + assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) + assert_equal("GET / ", res.body, log.call) + } + assert_equal(1, proxy_handler_called, log.call) + assert_equal(2, request_handler_called, log.call) + + req = Net::HTTP::Head.new("/") + http.request(req){|res| + assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) + assert_nil(res.body, log.call) + } + assert_equal(2, proxy_handler_called, log.call) + assert_equal(4, request_handler_called, log.call) + + req = Net::HTTP::Post.new("/") + req.body = "post-data" + req.content_type = "application/x-www-form-urlencoded" + http.request(req){|res| + assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) + assert_equal("POST / post-data", res.body, log.call) + } + assert_equal(3, proxy_handler_called, log.call) + assert_equal(6, request_handler_called, log.call) + } + end + + def test_no_proxy + # Testing GET or POST to the proxy server without proxy request. + # + # client -------> proxy + # GET / POST + # + proxy_handler_called = request_handler_called = 0 + config = { + :ServerName => "localhost.localdomain", + :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, + :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } + } + TestWEBrick.start_httpproxy(config){|server, addr, port, log| + server.mount_proc("/"){|req, res| + res.body = "#{req.request_method} #{req.path} #{req.body}" + } + http = Net::HTTP.new(addr, port) + + req = Net::HTTP::Get.new("/") + http.request(req){|res| + assert_nil(res["via"], log.call) + assert_equal("GET / ", res.body, log.call) + } + assert_equal(0, proxy_handler_called, log.call) + assert_equal(1, request_handler_called, log.call) + + req = Net::HTTP::Head.new("/") + http.request(req){|res| + assert_nil(res["via"], log.call) + assert_nil(res.body, log.call) + } + assert_equal(0, proxy_handler_called, log.call) + assert_equal(2, request_handler_called, log.call) + + req = Net::HTTP::Post.new("/") + req.content_type = "application/x-www-form-urlencoded" + req.body = "post-data" + http.request(req){|res| + assert_nil(res["via"], log.call) + assert_equal("POST / post-data", res.body, log.call) + } + assert_equal(0, proxy_handler_called, log.call) + assert_equal(3, request_handler_called, log.call) + } + end + + def test_big_bodies + require 'digest/md5' + rand_str = File.read(__FILE__) + rand_str.freeze + nr = 1024 ** 2 / rand_str.size # bigger works, too + exp = Digest::MD5.new + nr.times { exp.update(rand_str) } + exp = exp.hexdigest + TestWEBrick.start_httpserver do |o_server, o_addr, o_port, o_log| + o_server.mount_proc('/') do |req, res| + case req.request_method + when 'GET' + res['content-type'] = 'application/octet-stream' + if req.path == '/length' + res['content-length'] = (nr * rand_str.size).to_s + else + res.chunked = true + end + res.body = ->(socket) { nr.times { socket.write(rand_str) } } + when 'POST' + dig = Digest::MD5.new + req.body { |buf| dig.update(buf); buf.clear } + res['content-type'] = 'text/plain' + res['content-length'] = '32' + res.body = dig.hexdigest + end + end + + http = Net::HTTP.new(o_addr, o_port) + IO.pipe do |rd, wr| + headers = { + 'Content-Type' => 'application/octet-stream', + 'Transfer-Encoding' => 'chunked', + } + post = Net::HTTP::Post.new('/', headers) + th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } + post.body_stream = rd + http.request(post) do |res| + assert_equal 'text/plain', res['content-type'] + assert_equal 32, res.content_length + assert_equal exp, res.body + end + assert_nil th.value + end + + TestWEBrick.start_httpproxy do |p_server, p_addr, p_port, p_log| + http = Net::HTTP.new(o_addr, o_port, p_addr, p_port) + http.request_get('/length') do |res| + assert_equal(nr * rand_str.size, res.content_length) + dig = Digest::MD5.new + res.read_body { |buf| dig.update(buf); buf.clear } + assert_equal exp, dig.hexdigest + end + http.request_get('/') do |res| + assert_predicate res, :chunked? + dig = Digest::MD5.new + res.read_body { |buf| dig.update(buf); buf.clear } + assert_equal exp, dig.hexdigest + end + + IO.pipe do |rd, wr| + headers = { + 'Content-Type' => 'application/octet-stream', + 'Content-Length' => (nr * rand_str.size).to_s, + } + post = Net::HTTP::Post.new('/', headers) + th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } + post.body_stream = rd + http.request(post) do |res| + assert_equal 'text/plain', res['content-type'] + assert_equal 32, res.content_length + assert_equal exp, res.body + end + assert_nil th.value + end + + IO.pipe do |rd, wr| + headers = { + 'Content-Type' => 'application/octet-stream', + 'Transfer-Encoding' => 'chunked', + } + post = Net::HTTP::Post.new('/', headers) + th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } + post.body_stream = rd + http.request(post) do |res| + assert_equal 'text/plain', res['content-type'] + assert_equal 32, res.content_length + assert_equal exp, res.body + end + assert_nil th.value + end + end + end + end if RUBY_VERSION >= '2.5' + + def test_http10_proxy_chunked + # Testing HTTP/1.0 client request and HTTP/1.1 chunked response + # from origin server. + # +------+ + # V | + # client -------> proxy ---+ + # GET GET + # HTTP/1.0 HTTP/1.1 + # non-chunked chunked + # + proxy_handler_called = request_handler_called = 0 + config = { + :ServerName => "localhost.localdomain", + :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, + :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } + } + log_tester = lambda {|log, access_log| + log.reject! {|str| + %r{WARN chunked is set for an HTTP/1\.0 request\. \(ignored\)} =~ str + } + assert_equal([], log) + } + TestWEBrick.start_httpproxy(config, log_tester){|server, addr, port, log| + body = nil + server.mount_proc("/"){|req, res| + body = "#{req.request_method} #{req.path} #{req.body}" + res.chunked = true + res.body = -> (socket) { body.each_char {|c| socket.write c } } + } + + # Don't use Net::HTTP because it uses HTTP/1.1. + TCPSocket.open(addr, port) {|s| + s.write "GET / HTTP/1.0\r\nHost: localhost.localdomain\r\n\r\n" + response = s.read + assert_equal(body, response[/.*\z/]) + } + } + end + + def make_certificate(key, cn) + subject = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{cn}") + exts = [ + ["keyUsage", "keyEncipherment,digitalSignature", true], + ] + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 1 + cert.subject = subject + cert.issuer = subject + cert.public_key = key + cert.not_before = Time.now - 3600 + cert.not_after = Time.now + 3600 + ef = OpenSSL::X509::ExtensionFactory.new(cert, cert) + exts.each {|args| cert.add_extension(ef.create_extension(*args)) } + cert.sign(key, "sha256") + return cert + end if defined?(OpenSSL::SSL) + + def test_connect + # Testing CONNECT to proxy server + # + # client -----------> proxy -----------> https + # 1. CONNECT establish TCP + # 2. ---- establish SSL session ---> + # 3. ------- GET or POST ----------> + # + key = TEST_KEY_RSA2048 + cert = make_certificate(key, "127.0.0.1") + s_config = { + :SSLEnable =>true, + :ServerName => "localhost", + :SSLCertificate => cert, + :SSLPrivateKey => key, + } + config = { + :ServerName => "localhost.localdomain", + :RequestCallback => Proc.new{|req, res| + assert_equal("CONNECT", req.request_method) + }, + } + TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log| + s_server.mount_proc("/"){|req, res| + res.body = "SSL #{req.request_method} #{req.path} #{req.body}" + } + TestWEBrick.start_httpproxy(config){|server, addr, port, log| + http = Net::HTTP.new("127.0.0.1", s_port, addr, port) + http.use_ssl = true + http.verify_callback = Proc.new do |preverify_ok, store_ctx| + store_ctx.current_cert.to_der == cert.to_der + end + + req = Net::HTTP::Get.new("/") + req["Content-Type"] = "application/x-www-form-urlencoded" + http.request(req){|res| + assert_equal("SSL GET / ", res.body, s_log.call + log.call) + } + + req = Net::HTTP::Post.new("/") + req["Content-Type"] = "application/x-www-form-urlencoded" + req.body = "post-data" + http.request(req){|res| + assert_equal("SSL POST / post-data", res.body, s_log.call + log.call) + } + } + } + end if defined?(OpenSSL::SSL) + + def test_upstream_proxy + # Testing GET or POST through the upstream proxy server + # Note that the upstream proxy server works as the origin server. + # +------+ + # V | + # client -------> proxy -------> proxy ---+ + # GET / POST GET / POST GET / POST + # + up_proxy_handler_called = up_request_handler_called = 0 + proxy_handler_called = request_handler_called = 0 + up_config = { + :ServerName => "localhost.localdomain", + :ProxyContentHandler => Proc.new{|req, res| up_proxy_handler_called += 1}, + :RequestCallback => Proc.new{|req, res| up_request_handler_called += 1} + } + TestWEBrick.start_httpproxy(up_config){|up_server, up_addr, up_port, up_log| + up_server.mount_proc("/"){|req, res| + res.body = "#{req.request_method} #{req.path} #{req.body}" + } + config = { + :ServerName => "localhost.localdomain", + :ProxyURI => URI.parse("http://localhost:#{up_port}"), + :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1}, + :RequestCallback => Proc.new{|req, res| request_handler_called += 1}, + } + TestWEBrick.start_httpproxy(config){|server, addr, port, log| + http = Net::HTTP.new(up_addr, up_port, addr, port) + + req = Net::HTTP::Get.new("/") + http.request(req){|res| + skip res.message unless res.code == '200' + via = res["via"].split(/,\s+/) + assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) + assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) + assert_equal("GET / ", res.body) + } + assert_equal(1, up_proxy_handler_called, up_log.call + log.call) + assert_equal(2, up_request_handler_called, up_log.call + log.call) + assert_equal(1, proxy_handler_called, up_log.call + log.call) + assert_equal(1, request_handler_called, up_log.call + log.call) + + req = Net::HTTP::Head.new("/") + http.request(req){|res| + via = res["via"].split(/,\s+/) + assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) + assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) + assert_nil(res.body, up_log.call + log.call) + } + assert_equal(2, up_proxy_handler_called, up_log.call + log.call) + assert_equal(4, up_request_handler_called, up_log.call + log.call) + assert_equal(2, proxy_handler_called, up_log.call + log.call) + assert_equal(2, request_handler_called, up_log.call + log.call) + + req = Net::HTTP::Post.new("/") + req.body = "post-data" + req.content_type = "application/x-www-form-urlencoded" + http.request(req){|res| + via = res["via"].split(/,\s+/) + assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) + assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) + assert_equal("POST / post-data", res.body, up_log.call + log.call) + } + assert_equal(3, up_proxy_handler_called, up_log.call + log.call) + assert_equal(6, up_request_handler_called, up_log.call + log.call) + assert_equal(3, proxy_handler_called, up_log.call + log.call) + assert_equal(3, request_handler_called, up_log.call + log.call) + + if defined?(OpenSSL::SSL) + # Testing CONNECT to the upstream proxy server + # + # client -------> proxy -------> proxy -------> https + # 1. CONNECT CONNECT establish TCP + # 2. -------- establish SSL session ------> + # 3. ---------- GET or POST --------------> + # + key = TEST_KEY_RSA2048 + cert = make_certificate(key, "127.0.0.1") + s_config = { + :SSLEnable =>true, + :ServerName => "localhost", + :SSLCertificate => cert, + :SSLPrivateKey => key, + } + TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log| + s_server.mount_proc("/"){|req2, res| + res.body = "SSL #{req2.request_method} #{req2.path} #{req2.body}" + } + http = Net::HTTP.new("127.0.0.1", s_port, addr, port, up_log.call + log.call + s_log.call) + http.use_ssl = true + http.verify_callback = Proc.new do |preverify_ok, store_ctx| + store_ctx.current_cert.to_der == cert.to_der + end + + req2 = Net::HTTP::Get.new("/") + http.request(req2){|res| + assert_equal("SSL GET / ", res.body, up_log.call + log.call + s_log.call) + } + + req2 = Net::HTTP::Post.new("/") + req2.body = "post-data" + req2.content_type = "application/x-www-form-urlencoded" + http.request(req2){|res| + assert_equal("SSL POST / post-data", res.body, up_log.call + log.call + s_log.call) + } + } + end + } + } + end + + if defined?(OpenSSL::SSL) + TEST_KEY_RSA2048 = OpenSSL::PKey.read <<-_end_of_pem_ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuV9ht9J7k4NBs38jOXvvTKY9gW8nLICSno5EETR1cuF7i4pN +s9I1QJGAFAX0BEO4KbzXmuOvfCpD3CU+Slp1enenfzq/t/e/1IRW0wkJUJUFQign +4CtrkJL+P07yx18UjyPlBXb81ApEmAB5mrJVSrWmqbjs07JbuS4QQGGXLc+Su96D +kYKmSNVjBiLxVVSpyZfAY3hD37d60uG+X8xdW5v68JkRFIhdGlb6JL8fllf/A/bl +NwdJOhVr9mESHhwGjwfSeTDPfd8ZLE027E5lyAVX9KZYcU00mOX+fdxOSnGqS/8J +DRh0EPHDL15RcJjV2J6vZjPb0rOYGDoMcH+94wIDAQABAoIBAAzsamqfYQAqwXTb +I0CJtGg6msUgU7HVkOM+9d3hM2L791oGHV6xBAdpXW2H8LgvZHJ8eOeSghR8+dgq +PIqAffo4x1Oma+FOg3A0fb0evyiACyrOk+EcBdbBeLo/LcvahBtqnDfiUMQTpy6V +seSoFCwuN91TSCeGIsDpRjbG1vxZgtx+uI+oH5+ytqJOmfCksRDCkMglGkzyfcl0 +Xc5CUhIJ0my53xijEUQl19rtWdMnNnnkdbG8PT3LZlOta5Do86BElzUYka0C6dUc +VsBDQ0Nup0P6rEQgy7tephHoRlUGTYamsajGJaAo1F3IQVIrRSuagi7+YpSpCqsW +wORqorkCgYEA7RdX6MDVrbw7LePnhyuaqTiMK+055/R1TqhB1JvvxJ1CXk2rDL6G +0TLHQ7oGofd5LYiemg4ZVtWdJe43BPZlVgT6lvL/iGo8JnrncB9Da6L7nrq/+Rvj +XGjf1qODCK+LmreZWEsaLPURIoR/Ewwxb9J2zd0CaMjeTwafJo1CZvcCgYEAyCgb +aqoWvUecX8VvARfuA593Lsi50t4MEArnOXXcd1RnXoZWhbx5rgO8/ATKfXr0BK/n +h2GF9PfKzHFm/4V6e82OL7gu/kLy2u9bXN74vOvWFL5NOrOKPM7Kg+9I131kNYOw +Ivnr/VtHE5s0dY7JChYWE1F3vArrOw3T00a4CXUCgYEA0SqY+dS2LvIzW4cHCe9k +IQqsT0yYm5TFsUEr4sA3xcPfe4cV8sZb9k/QEGYb1+SWWZ+AHPV3UW5fl8kTbSNb +v4ng8i8rVVQ0ANbJO9e5CUrepein2MPL0AkOATR8M7t7dGGpvYV0cFk8ZrFx0oId +U0PgYDotF/iueBWlbsOM430CgYEAqYI95dFyPI5/AiSkY5queeb8+mQH62sdcCCr +vd/w/CZA/K5sbAo4SoTj8dLk4evU6HtIa0DOP63y071eaxvRpTNqLUOgmLh+D6gS +Cc7TfLuFrD+WDBatBd5jZ+SoHccVrLR/4L8jeodo5FPW05A+9gnKXEXsTxY4LOUC +9bS4e1kCgYAqVXZh63JsMwoaxCYmQ66eJojKa47VNrOeIZDZvd2BPVf30glBOT41 +gBoDG3WMPZoQj9pb7uMcrnvs4APj2FIhMU8U15LcPAj59cD6S6rWnAxO8NFK7HQG +4Jxg3JNNf8ErQoCHb1B3oVdXJkmbJkARoDpBKmTCgKtP8ADYLmVPQw== +-----END RSA PRIVATE KEY----- + _end_of_pem_ + end +end diff --git a/tool/test/webrick/test_httprequest.rb b/tool/test/webrick/test_httprequest.rb new file mode 100644 index 0000000000..759ccbdada --- /dev/null +++ b/tool/test/webrick/test_httprequest.rb @@ -0,0 +1,488 @@ +# frozen_string_literal: false +require "webrick" +require "stringio" +require "test/unit" + +class TestWEBrickHTTPRequest < Test::Unit::TestCase + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def test_simple_request + msg = <<-_end_of_message_ +GET / + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert(req.meta_vars) # fails if @header was not initialized and iteration is attempted on the nil reference + end + + def test_parse_09 + msg = <<-_end_of_message_ + GET / + foobar # HTTP/0.9 request don't have header nor entity body. + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("GET", req.request_method) + assert_equal("/", req.unparsed_uri) + assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version) + assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) + assert_equal(80, req.port) + assert_equal(false, req.keep_alive?) + assert_equal(nil, req.body) + assert(req.query.empty?) + end + + def test_parse_10 + msg = <<-_end_of_message_ + GET / HTTP/1.0 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("GET", req.request_method) + assert_equal("/", req.unparsed_uri) + assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version) + assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) + assert_equal(80, req.port) + assert_equal(false, req.keep_alive?) + assert_equal(nil, req.body) + assert(req.query.empty?) + end + + def test_parse_11 + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("GET", req.request_method) + assert_equal("/path", req.unparsed_uri) + assert_equal("", req.script_name) + assert_equal("/path", req.path_info) + assert_equal(WEBrick::HTTPVersion.new("1.1"), req.http_version) + assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) + assert_equal(80, req.port) + assert_equal(true, req.keep_alive?) + assert_equal(nil, req.body) + assert(req.query.empty?) + end + + def test_request_uri_too_large + msg = <<-_end_of_message_ + GET /#{"a"*2084} HTTP/1.1 + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){ + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + } + end + + def test_parse_headers + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: test.ruby-lang.org:8080 + Connection: close + Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, + text/html;level=2;q=0.4, */*;q=0.5 + Accept-Encoding: compress;q=0.5 + Accept-Encoding: gzip;q=1.0, identity; q=0.4, *;q=0 + Accept-Language: en;q=0.5, *; q=0 + Accept-Language: ja + Content-Type: text/plain + Content-Length: 7 + X-Empty-Header: + + foobar + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal( + URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri) + assert_equal("test.ruby-lang.org", req.host) + assert_equal(8080, req.port) + assert_equal(false, req.keep_alive?) + assert_equal( + %w(text/html;level=1 text/html */* text/html;level=2 text/*), + req.accept) + assert_equal(%w(gzip compress identity *), req.accept_encoding) + assert_equal(%w(ja en *), req.accept_language) + assert_equal(7, req.content_length) + assert_equal("text/plain", req.content_type) + assert_equal("foobar\n", req.body) + assert_equal("", req["x-empty-header"]) + assert_equal(nil, req["x-no-header"]) + assert(req.query.empty?) + end + + def test_parse_header2() + msg = <<-_end_of_message_ + POST /foo/bar/../baz?q=a HTTP/1.0 + Content-Length: 9 + User-Agent: + FOO BAR + BAZ + + hogehoge + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal("POST", req.request_method) + assert_equal("/foo/baz", req.path) + assert_equal("", req.script_name) + assert_equal("/foo/baz", req.path_info) + assert_equal("9", req['content-length']) + assert_equal("FOO BAR BAZ", req['user-agent']) + assert_equal("hogehoge\n", req.body) + end + + def test_parse_headers3 + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: test.ruby-lang.org + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri) + assert_equal("test.ruby-lang.org", req.host) + assert_equal(80, req.port) + + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: 192.168.1.1 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri) + assert_equal("192.168.1.1", req.host) + assert_equal(80, req.port) + + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: [fe80::208:dff:feef:98c7] + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"), + req.request_uri) + assert_equal("[fe80::208:dff:feef:98c7]", req.host) + assert_equal(80, req.port) + + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: 192.168.1.1:8080 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri) + assert_equal("192.168.1.1", req.host) + assert_equal(8080, req.port) + + msg = <<-_end_of_message_ + GET /path HTTP/1.1 + Host: [fe80::208:dff:feef:98c7]:8080 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"), + req.request_uri) + assert_equal("[fe80::208:dff:feef:98c7]", req.host) + assert_equal(8080, req.port) + end + + def test_parse_get_params + param = "foo=1;foo=2;foo=3;bar=x" + msg = <<-_end_of_message_ + GET /path?#{param} HTTP/1.1 + Host: test.ruby-lang.org:8080 + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + query = req.query + assert_equal("1", query["foo"]) + assert_equal(["1", "2", "3"], query["foo"].to_ary) + assert_equal(["1", "2", "3"], query["foo"].list) + assert_equal("x", query["bar"]) + assert_equal(["x"], query["bar"].list) + end + + def test_parse_post_params + param = "foo=1;foo=2;foo=3;bar=x" + msg = <<-_end_of_message_ + POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 + Host: test.ruby-lang.org:8080 + Content-Length: #{param.size} + Content-Type: application/x-www-form-urlencoded + + #{param} + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + query = req.query + assert_equal("1", query["foo"]) + assert_equal(["1", "2", "3"], query["foo"].to_ary) + assert_equal(["1", "2", "3"], query["foo"].list) + assert_equal("x", query["bar"]) + assert_equal(["x"], query["bar"].list) + end + + def test_chunked + crlf = "\x0d\x0a" + expect = File.binread(__FILE__).freeze + msg = <<-_end_of_message_ + POST /path HTTP/1.1 + Host: test.ruby-lang.org:8080 + Transfer-Encoding: chunked + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + open(__FILE__){|io| + while chunk = io.read(100) + msg << chunk.size.to_s(16) << crlf + msg << chunk << crlf + end + } + msg << "0" << crlf + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal(expect, req.body) + + # chunked req.body_reader + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + dst = StringIO.new + IO.copy_stream(req.body_reader, dst) + assert_equal(expect, dst.string) + end + + def test_forwarded + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + User-Agent: w3m/0.5.2 + X-Forwarded-For: 123.123.123.123 + X-Forwarded-Host: forward.example.com + X-Forwarded-Server: server.example.com + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server.example.com", req.server_name) + assert_equal("http://forward.example.com/foo", req.request_uri.to_s) + assert_equal("forward.example.com", req.host) + assert_equal(80, req.port) + assert_equal("123.123.123.123", req.remote_ip) + assert(!req.ssl?) + + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + User-Agent: w3m/0.5.2 + X-Forwarded-For: 192.168.1.10, 172.16.1.1, 123.123.123.123 + X-Forwarded-Host: forward.example.com:8080 + X-Forwarded-Server: server.example.com + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server.example.com", req.server_name) + assert_equal("http://forward.example.com:8080/foo", req.request_uri.to_s) + assert_equal("forward.example.com", req.host) + assert_equal(8080, req.port) + assert_equal("123.123.123.123", req.remote_ip) + assert(!req.ssl?) + + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + Client-IP: 234.234.234.234 + X-Forwarded-Proto: https, http + X-Forwarded-For: 192.168.1.10, 10.0.0.1, 123.123.123.123 + X-Forwarded-Host: forward.example.com + X-Forwarded-Server: server.example.com + X-Requested-With: XMLHttpRequest + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server.example.com", req.server_name) + assert_equal("https://forward.example.com/foo", req.request_uri.to_s) + assert_equal("forward.example.com", req.host) + assert_equal(443, req.port) + assert_equal("234.234.234.234", req.remote_ip) + assert(req.ssl?) + + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + Client-IP: 234.234.234.234 + X-Forwarded-Proto: https + X-Forwarded-For: 192.168.1.10 + X-Forwarded-Host: forward1.example.com:1234, forward2.example.com:5678 + X-Forwarded-Server: server1.example.com, server2.example.com + X-Requested-With: XMLHttpRequest + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server1.example.com", req.server_name) + assert_equal("https://forward1.example.com:1234/foo", req.request_uri.to_s) + assert_equal("forward1.example.com", req.host) + assert_equal(1234, req.port) + assert_equal("234.234.234.234", req.remote_ip) + assert(req.ssl?) + + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + Client-IP: 234.234.234.234 + X-Forwarded-Proto: https + X-Forwarded-For: 192.168.1.10 + X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84], forward2.example.com:5678 + X-Forwarded-Server: server1.example.com, server2.example.com + X-Requested-With: XMLHttpRequest + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server1.example.com", req.server_name) + assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]/foo", req.request_uri.to_s) + assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host) + assert_equal(443, req.port) + assert_equal("234.234.234.234", req.remote_ip) + assert(req.ssl?) + + msg = <<-_end_of_message_ + GET /foo HTTP/1.1 + Host: localhost:10080 + Client-IP: 234.234.234.234 + X-Forwarded-Proto: https + X-Forwarded-For: 192.168.1.10 + X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234, forward2.example.com:5678 + X-Forwarded-Server: server1.example.com, server2.example.com + X-Requested-With: XMLHttpRequest + Connection: Keep-Alive + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("server1.example.com", req.server_name) + assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234/foo", req.request_uri.to_s) + assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host) + assert_equal(1234, req.port) + assert_equal("234.234.234.234", req.remote_ip) + assert(req.ssl?) + end + + def test_continue_sent + msg = <<-_end_of_message_ + POST /path HTTP/1.1 + Expect: 100-continue + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert req['expect'] + l = msg.size + req.continue + assert_not_equal l, msg.size + assert_match(/HTTP\/1.1 100 continue\r\n\r\n\z/, msg) + assert !req['expect'] + end + + def test_continue_not_sent + msg = <<-_end_of_message_ + POST /path HTTP/1.1 + + _end_of_message_ + msg.gsub!(/^ {6}/, "") + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert !req['expect'] + l = msg.size + req.continue + assert_equal l, msg.size + end + + def test_empty_post + msg = <<-_end_of_message_ + POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 + Host: test.ruby-lang.org:8080 + Content-Type: application/x-www-form-urlencoded + + _end_of_message_ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.body + end + + def test_bad_messages + param = "foo=1;foo=2;foo=3;bar=x" + msg = <<-_end_of_message_ + POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 + Host: test.ruby-lang.org:8080 + Content-Type: application/x-www-form-urlencoded + + #{param} + _end_of_message_ + assert_raise(WEBrick::HTTPStatus::LengthRequired){ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.body + } + + msg = <<-_end_of_message_ + POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 + Host: test.ruby-lang.org:8080 + Content-Length: 100000 + + body is too short. + _end_of_message_ + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.body + } + + msg = <<-_end_of_message_ + POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 + Host: test.ruby-lang.org:8080 + Transfer-Encoding: foobar + + body is too short. + _end_of_message_ + assert_raise(WEBrick::HTTPStatus::NotImplemented){ + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.body + } + end + + def test_eof_raised_when_line_is_nil + assert_raise(WEBrick::HTTPStatus::EOFError) { + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new("")) + } + end +end diff --git a/tool/test/webrick/test_httpresponse.rb b/tool/test/webrick/test_httpresponse.rb new file mode 100644 index 0000000000..89a0f7036e --- /dev/null +++ b/tool/test/webrick/test_httpresponse.rb @@ -0,0 +1,282 @@ +# frozen_string_literal: false +require "webrick" +require "minitest/autorun" +require "stringio" +require "net/http" + +module WEBrick + class TestHTTPResponse < MiniTest::Unit::TestCase + class FakeLogger + attr_reader :messages + + def initialize + @messages = [] + end + + def warn msg + @messages << msg + end + end + + attr_reader :config, :logger, :res + + def setup + super + @logger = FakeLogger.new + @config = Config::HTTP + @config[:Logger] = logger + @res = HTTPResponse.new config + @res.keep_alive = true + end + + def test_prevent_response_splitting_headers_crlf + res['X-header'] = "malicious\r\nCookie: cracked_indicator_for_test" + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_prevent_response_splitting_cookie_headers_crlf + user_input = "malicious\r\nCookie: cracked_indicator_for_test" + res.cookies << WEBrick::Cookie.new('author', user_input) + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_prevent_response_splitting_headers_cr + res['X-header'] = "malicious\rCookie: cracked_indicator_for_test" + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_prevent_response_splitting_cookie_headers_cr + user_input = "malicious\rCookie: cracked_indicator_for_test" + res.cookies << WEBrick::Cookie.new('author', user_input) + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_prevent_response_splitting_headers_lf + res['X-header'] = "malicious\nCookie: cracked_indicator_for_test" + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_prevent_response_splitting_cookie_headers_lf + user_input = "malicious\nCookie: cracked_indicator_for_test" + res.cookies << WEBrick::Cookie.new('author', user_input) + io = StringIO.new + res.send_response io + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '500', res.code + refute_match 'cracked_indicator_for_test', io.string + end + + def test_set_redirect_response_splitting + url = "malicious\r\nCookie: cracked_indicator_for_test" + assert_raises(URI::InvalidURIError) do + res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) + end + end + + def test_set_redirect_html_injection + url = 'http://example.com////?a</a><head></head><body><img src=1></body>' + assert_raises(WEBrick::HTTPStatus::MultipleChoices) do + res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) + end + res.status = 300 + io = StringIO.new + res.send_response(io) + io.rewind + res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) + assert_equal '300', res.code + refute_match(/<img/, io.string) + end + + def test_304_does_not_log_warning + res.status = 304 + res.setup_header + assert_equal 0, logger.messages.length + end + + def test_204_does_not_log_warning + res.status = 204 + res.setup_header + + assert_equal 0, logger.messages.length + end + + def test_1xx_does_not_log_warnings + res.status = 105 + res.setup_header + + assert_equal 0, logger.messages.length + end + + def test_200_chunked_does_not_set_content_length + res.chunked = false + res["Transfer-Encoding"] = 'chunked' + res.setup_header + assert_nil res.header.fetch('content-length', nil) + end + + def test_send_body_io + IO.pipe {|body_r, body_w| + body_w.write 'hello' + body_w.close + + @res.body = body_r + + IO.pipe {|r, w| + + @res.send_body w + + w.close + + assert_equal 'hello', r.read + } + } + assert_equal 0, logger.messages.length + end + + def test_send_body_string + @res.body = 'hello' + + IO.pipe {|r, w| + @res.send_body w + + w.close + + assert_equal 'hello', r.read + } + assert_equal 0, logger.messages.length + end + + def test_send_body_string_io + @res.body = StringIO.new 'hello' + + IO.pipe {|r, w| + @res.send_body w + + w.close + + assert_equal 'hello', r.read + } + assert_equal 0, logger.messages.length + end + + def test_send_body_io_chunked + @res.chunked = true + + IO.pipe {|body_r, body_w| + + body_w.write 'hello' + body_w.close + + @res.body = body_r + + IO.pipe {|r, w| + @res.send_body w + + w.close + + r.binmode + assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read + } + } + assert_equal 0, logger.messages.length + end + + def test_send_body_string_chunked + @res.chunked = true + + @res.body = 'hello' + + IO.pipe {|r, w| + @res.send_body w + + w.close + + r.binmode + assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read + } + assert_equal 0, logger.messages.length + end + + def test_send_body_string_io_chunked + @res.chunked = true + + @res.body = StringIO.new 'hello' + + IO.pipe {|r, w| + @res.send_body w + + w.close + + r.binmode + assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read + } + assert_equal 0, logger.messages.length + end + + def test_send_body_proc + @res.body = Proc.new { |out| out.write('hello') } + IO.pipe do |r, w| + @res.send_body(w) + w.close + r.binmode + assert_equal 'hello', r.read + end + assert_equal 0, logger.messages.length + end + + def test_send_body_proc_chunked + @res.body = Proc.new { |out| out.write('hello') } + @res.chunked = true + IO.pipe do |r, w| + @res.send_body(w) + w.close + r.binmode + assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read + end + assert_equal 0, logger.messages.length + end + + def test_set_error + status = 400 + message = 'missing attribute' + @res.status = status + error = WEBrick::HTTPStatus[status].new(message) + body = @res.set_error(error) + assert_match(/#{@res.reason_phrase}/, body) + assert_match(/#{message}/, body) + end + + def test_no_extraneous_space + [200, 300, 400, 500].each do |status| + @res.status = status + assert_match(/\S\r\n/, @res.status_line) + end + end + end +end diff --git a/tool/test/webrick/test_https.rb b/tool/test/webrick/test_https.rb new file mode 100644 index 0000000000..ec0aac354a --- /dev/null +++ b/tool/test/webrick/test_https.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: false +require "test/unit" +require "net/http" +require "webrick" +require "webrick/https" +require "webrick/utils" +require_relative "utils" + +class TestWEBrickHTTPS < Test::Unit::TestCase + empty_log = Object.new + def empty_log.<<(str) + assert_equal('', str) + self + end + NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN) + + class HTTPSNITest < ::Net::HTTP + attr_accessor :sni_hostname + + def ssl_socket_connect(s, timeout) + s.hostname = sni_hostname + super + end + end + + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def https_get(addr, port, hostname, path, verifyname = nil) + subject = nil + http = HTTPSNITest.new(addr, port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.verify_callback = proc { |x, store| subject = store.chain[0].subject.to_s; x } + http.sni_hostname = hostname + req = Net::HTTP::Get.new(path) + req["Host"] = "#{hostname}:#{port}" + response = http.start { http.request(req).body } + assert_equal("/CN=#{verifyname || hostname}", subject) + response + end + + def test_sni + config = { + :ServerName => "localhost", + :SSLEnable => true, + :SSLCertName => "/CN=localhost", + } + TestWEBrick.start_httpserver(config){|server, addr, port, log| + server.mount_proc("/") {|req, res| res.body = "master" } + + # catch stderr in create_self_signed_cert + stderr_buffer = StringIO.new + old_stderr, $stderr = $stderr, stderr_buffer + + begin + vhost_config1 = { + :ServerName => "vhost1", + :Port => port, + :DoNotListen => true, + :Logger => NoLog, + :AccessLog => [], + :SSLEnable => true, + :SSLCertName => "/CN=vhost1", + } + vhost1 = WEBrick::HTTPServer.new(vhost_config1) + vhost1.mount_proc("/") {|req, res| res.body = "vhost1" } + server.virtual_host(vhost1) + + vhost_config2 = { + :ServerName => "vhost2", + :ServerAlias => ["vhost2alias"], + :Port => port, + :DoNotListen => true, + :Logger => NoLog, + :AccessLog => [], + :SSLEnable => true, + :SSLCertName => "/CN=vhost2", + } + vhost2 = WEBrick::HTTPServer.new(vhost_config2) + vhost2.mount_proc("/") {|req, res| res.body = "vhost2" } + server.virtual_host(vhost2) + ensure + # restore stderr + $stderr = old_stderr + end + + assert_match(/\A([.+*]+\n)+\z/, stderr_buffer.string) + assert_equal("master", https_get(addr, port, "localhost", "/localhost")) + assert_equal("master", https_get(addr, port, "unknown", "/unknown", "localhost")) + assert_equal("vhost1", https_get(addr, port, "vhost1", "/vhost1")) + assert_equal("vhost2", https_get(addr, port, "vhost2", "/vhost2")) + assert_equal("vhost2", https_get(addr, port, "vhost2alias", "/vhost2alias", "vhost2")) + } + end + + def test_check_ssl_virtual + config = { + :ServerName => "localhost", + :SSLEnable => true, + :SSLCertName => "/CN=localhost", + } + TestWEBrick.start_httpserver(config){|server, addr, port, log| + assert_raise ArgumentError do + vhost = WEBrick::HTTPServer.new({:DoNotListen => true, :Logger => NoLog}) + server.virtual_host(vhost) + end + } + end +end diff --git a/tool/test/webrick/test_httpserver.rb b/tool/test/webrick/test_httpserver.rb new file mode 100644 index 0000000000..4133be85ad --- /dev/null +++ b/tool/test/webrick/test_httpserver.rb @@ -0,0 +1,543 @@ +# frozen_string_literal: false +require "test/unit" +require "net/http" +require "webrick" +require_relative "utils" + +class TestWEBrickHTTPServer < Test::Unit::TestCase + empty_log = Object.new + def empty_log.<<(str) + assert_equal('', str) + self + end + NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN) + + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def test_mount + httpd = WEBrick::HTTPServer.new( + :Logger => NoLog, + :DoNotListen=>true + ) + httpd.mount("/", :Root) + httpd.mount("/foo", :Foo) + httpd.mount("/foo/bar", :Bar, :bar1) + httpd.mount("/foo/bar/baz", :Baz, :baz1, :baz2) + + serv, opts, script_name, path_info = httpd.search_servlet("/") + assert_equal(:Root, serv) + assert_equal([], opts) + assert_equal("", script_name) + assert_equal("/", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/sub") + assert_equal(:Root, serv) + assert_equal([], opts) + assert_equal("", script_name) + assert_equal("/sub", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/sub/") + assert_equal(:Root, serv) + assert_equal([], opts) + assert_equal("", script_name) + assert_equal("/sub/", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/foo") + assert_equal(:Foo, serv) + assert_equal([], opts) + assert_equal("/foo", script_name) + assert_equal("", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/foo/") + assert_equal(:Foo, serv) + assert_equal([], opts) + assert_equal("/foo", script_name) + assert_equal("/", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/foo/sub") + assert_equal(:Foo, serv) + assert_equal([], opts) + assert_equal("/foo", script_name) + assert_equal("/sub", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar") + assert_equal(:Bar, serv) + assert_equal([:bar1], opts) + assert_equal("/foo/bar", script_name) + assert_equal("", path_info) + + serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar/baz") + assert_equal(:Baz, serv) + assert_equal([:baz1, :baz2], opts) + assert_equal("/foo/bar/baz", script_name) + assert_equal("", path_info) + end + + class Req + attr_reader :port, :host + def initialize(addr, port, host) + @addr, @port, @host = addr, port, host + end + def addr + [0,0,0,@addr] + end + end + + def httpd(addr, port, host, ali) + config ={ + :Logger => NoLog, + :DoNotListen => true, + :BindAddress => addr, + :Port => port, + :ServerName => host, + :ServerAlias => ali, + } + return WEBrick::HTTPServer.new(config) + end + + def assert_eql?(v1, v2) + assert_equal(v1.object_id, v2.object_id) + end + + def test_lookup_server + addr1 = "192.168.100.1" + addr2 = "192.168.100.2" + addrz = "192.168.100.254" + local = "127.0.0.1" + port1 = 80 + port2 = 8080 + port3 = 10080 + portz = 32767 + name1 = "www.example.com" + name2 = "www2.example.com" + name3 = "www3.example.com" + namea = "www.example.co.jp" + nameb = "www.example.jp" + namec = "www2.example.co.jp" + named = "www2.example.jp" + namez = "foobar.example.com" + alias1 = [namea, nameb] + alias2 = [namec, named] + + host1 = httpd(nil, port1, name1, nil) + hosts = [ + host2 = httpd(addr1, port1, name1, nil), + host3 = httpd(addr1, port1, name2, alias1), + host4 = httpd(addr1, port2, name1, nil), + host5 = httpd(addr1, port2, name2, alias1), + httpd(addr1, port2, name3, alias2), + host7 = httpd(addr2, nil, name1, nil), + host8 = httpd(addr2, nil, name2, alias1), + httpd(addr2, nil, name3, alias2), + host10 = httpd(local, nil, nil, nil), + host11 = httpd(nil, port3, nil, nil), + ].sort_by{ rand } + hosts.each{|h| host1.virtual_host(h) } + + # connect to addr1 + assert_eql?(host2, host1.lookup_server(Req.new(addr1, port1, name1))) + assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, name2))) + assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, namea))) + assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, port1, namez))) + assert_eql?(host4, host1.lookup_server(Req.new(addr1, port2, name1))) + assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, name2))) + assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, namea))) + assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, port2, namez))) + assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name1))) + assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name2))) + assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namea))) + assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, nameb))) + assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namez))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name1))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name2))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namea))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namez))) + + # connect to addr2 + assert_eql?(host7, host1.lookup_server(Req.new(addr2, port1, name1))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, name2))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, namea))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr2, port1, namez))) + assert_eql?(host7, host1.lookup_server(Req.new(addr2, port2, name1))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, name2))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, namea))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr2, port2, namez))) + assert_eql?(host7, host1.lookup_server(Req.new(addr2, port3, name1))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, name2))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, namea))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, nameb))) + assert_eql?(host11, host1.lookup_server(Req.new(addr2, port3, namez))) + assert_eql?(host7, host1.lookup_server(Req.new(addr2, portz, name1))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, name2))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, namea))) + assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addr2, portz, namez))) + + # connect to addrz + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name1))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name2))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namea))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namez))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name1))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name2))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namea))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namez))) + assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name1))) + assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name2))) + assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namea))) + assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, nameb))) + assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namez))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name1))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name2))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namea))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, nameb))) + assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namez))) + + # connect to localhost + assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name1))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name2))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namea))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port1, nameb))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namez))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name1))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name2))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namea))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port2, nameb))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namez))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name1))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name2))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namea))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port3, nameb))) + assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namez))) + assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name1))) + assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name2))) + assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namea))) + assert_eql?(host10, host1.lookup_server(Req.new(local, portz, nameb))) + assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namez))) + end + + def test_callbacks + accepted = started = stopped = 0 + requested0 = requested1 = 0 + config = { + :ServerName => "localhost", + :AcceptCallback => Proc.new{ accepted += 1 }, + :StartCallback => Proc.new{ started += 1 }, + :StopCallback => Proc.new{ stopped += 1 }, + :RequestCallback => Proc.new{|req, res| requested0 += 1 }, + } + log_tester = lambda {|log, access_log| + assert(log.find {|s| %r{ERROR `/' not found\.} =~ s }) + assert_equal([], log.reject {|s| %r{ERROR `/' not found\.} =~ s }) + } + TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| + vhost_config = { + :ServerName => "myhostname", + :BindAddress => addr, + :Port => port, + :DoNotListen => true, + :Logger => NoLog, + :AccessLog => [], + :RequestCallback => Proc.new{|req, res| requested1 += 1 }, + } + server.virtual_host(WEBrick::HTTPServer.new(vhost_config)) + + Thread.pass while server.status != :Running + sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait + assert_equal(1, started, log.call) + assert_equal(0, stopped, log.call) + assert_equal(0, accepted, log.call) + + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/") + req["Host"] = "myhostname:#{port}" + http.request(req){|res| assert_equal("404", res.code, log.call)} + http.request(req){|res| assert_equal("404", res.code, log.call)} + http.request(req){|res| assert_equal("404", res.code, log.call)} + req["Host"] = "localhost:#{port}" + http.request(req){|res| assert_equal("404", res.code, log.call)} + http.request(req){|res| assert_equal("404", res.code, log.call)} + http.request(req){|res| assert_equal("404", res.code, log.call)} + assert_equal(6, accepted, log.call) + assert_equal(3, requested0, log.call) + assert_equal(3, requested1, log.call) + } + assert_equal(started, 1) + assert_equal(stopped, 1) + end + + class CustomRequest < ::WEBrick::HTTPRequest; end + class CustomResponse < ::WEBrick::HTTPResponse; end + class CustomServer < ::WEBrick::HTTPServer + def create_request(config) + CustomRequest.new(config) + end + + def create_response(config) + CustomResponse.new(config) + end + end + + def test_custom_server_request_and_response + config = { :ServerName => "localhost" } + TestWEBrick.start_server(CustomServer, config){|server, addr, port, log| + server.mount_proc("/", lambda {|req, res| + assert_kind_of(CustomRequest, req) + assert_kind_of(CustomResponse, res) + res.body = "via custom response" + }) + Thread.pass while server.status != :Running + + Net::HTTP.start(addr, port) do |http| + req = Net::HTTP::Get.new("/") + http.request(req){|res| + assert_equal("via custom response", res.body) + } + server.shutdown + end + } + end + + # This class is needed by test_response_io_with_chunked_set method + class EventManagerForChunkedResponseTest + def initialize + @listeners = [] + end + def add_listener( &block ) + @listeners << block + end + def raise_str_event( str ) + @listeners.each{ |e| e.call( :str, str ) } + end + def raise_close_event() + @listeners.each{ |e| e.call( :cls ) } + end + end + def test_response_io_with_chunked_set + evt_man = EventManagerForChunkedResponseTest.new + t = Thread.new do + begin + config = { + :ServerName => "localhost" + } + TestWEBrick.start_httpserver(config) do |server, addr, port, log| + body_strs = [ 'aaaaaa', 'bb', 'cccc' ] + server.mount_proc( "/", ->( req, res ){ + # Test for setting chunked... + res.chunked = true + r,w = IO.pipe + evt_man.add_listener do |type,str| + type == :cls ? ( w.close ) : ( w << str ) + end + res.body = r + } ) + Thread.pass while server.status != :Running + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/") + http.request(req) do |res| + i = 0 + evt_man.raise_str_event( body_strs[i] ) + res.read_body do |s| + assert_equal( body_strs[i], s ) + i += 1 + if i < body_strs.length + evt_man.raise_str_event( body_strs[i] ) + else + evt_man.raise_close_event() + end + end + assert_equal( body_strs.length, i ) + end + end + rescue => err + flunk( 'exception raised in thread: ' + err.to_s ) + end + end + if t.join( 3 ).nil? + evt_man.raise_close_event() + flunk( 'timeout' ) + if t.join( 1 ).nil? + Thread.kill t + end + end + end + + def test_response_io_without_chunked_set + config = { + :ServerName => "localhost" + } + log_tester = lambda {|log, access_log| + assert_equal(1, log.length) + assert_match(/WARN Could not determine content-length of response body./, log[0]) + } + TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| + server.mount_proc("/", lambda { |req, res| + r,w = IO.pipe + # Test for not setting chunked... + # res.chunked = true + res.body = r + w << "foo" + w.close + }) + Thread.pass while server.status != :Running + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/") + req['Connection'] = 'Keep-Alive' + begin + Timeout.timeout(2) do + http.request(req){|res| assert_equal("foo", res.body) } + end + rescue Timeout::Error + flunk('corrupted response') + end + } + end + + def test_request_handler_callback_is_deprecated + requested = 0 + config = { + :ServerName => "localhost", + :RequestHandler => Proc.new{|req, res| requested += 1 }, + } + log_tester = lambda {|log, access_log| + assert_equal(2, log.length) + assert_match(/WARN :RequestHandler is deprecated, please use :RequestCallback/, log[0]) + assert_match(%r{ERROR `/' not found\.}, log[1]) + } + TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| + Thread.pass while server.status != :Running + + http = Net::HTTP.new(addr, port) + req = Net::HTTP::Get.new("/") + req["Host"] = "localhost:#{port}" + http.request(req){|res| assert_equal("404", res.code, log.call)} + assert_match(%r{:RequestHandler is deprecated, please use :RequestCallback$}, log.call, log.call) + } + assert_equal(1, requested) + end + + def test_shutdown_with_busy_keepalive_connection + requested = 0 + config = { + :ServerName => "localhost", + } + TestWEBrick.start_httpserver(config){|server, addr, port, log| + server.mount_proc("/", lambda {|req, res| res.body = "heffalump" }) + Thread.pass while server.status != :Running + + Net::HTTP.start(addr, port) do |http| + req = Net::HTTP::Get.new("/") + http.request(req){|res| assert_equal('Keep-Alive', res['Connection'], log.call) } + server.shutdown + begin + 10.times {|n| http.request(req); requested += 1 } + rescue + # Errno::ECONNREFUSED or similar + end + end + } + assert_equal(0, requested, "Server responded to #{requested} requests after shutdown") + end + + def test_cntrl_in_path + log_ary = [] + access_log_ary = [] + config = { + :Port => 0, + :BindAddress => '127.0.0.1', + :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN), + :AccessLog => [[access_log_ary, '']], + } + s = WEBrick::HTTPServer.new(config) + s.mount('/foo', WEBrick::HTTPServlet::FileHandler, __FILE__) + th = Thread.new { s.start } + addr = s.listeners[0].addr + + http = Net::HTTP.new(addr[3], addr[1]) + req = Net::HTTP::Get.new('/notexist%0a/foo') + http.request(req) { |res| assert_equal('404', res.code) } + exp = %Q(ERROR `/notexist\\n/foo' not found.\n) + assert_equal 1, log_ary.size + assert_include log_ary[0], exp + ensure + s&.shutdown + th&.join + end + + def test_gigantic_request_header + log_tester = lambda {|log, access_log| + assert_equal 1, log.size + assert_include log[0], 'ERROR headers too large' + } + TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log| + server.mount('/', WEBrick::HTTPServlet::FileHandler, __FILE__) + TCPSocket.open(addr, port) do |c| + c.write("GET / HTTP/1.0\r\n") + junk = -"X-Junk: #{' ' * 1024}\r\n" + assert_raise(Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE) do + loop { c.write(junk) } + end + end + } + end + + def test_eof_in_chunk + log_tester = lambda do |log, access_log| + assert_equal 1, log.size + assert_include log[0], 'ERROR bad chunk data size' + end + TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log| + server.mount_proc('/', ->(req, res) { res.body = req.body }) + TCPSocket.open(addr, port) do |c| + c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \ + "Transfer-Encoding: chunked\r\n\r\n5\r\na") + c.shutdown(Socket::SHUT_WR) # trigger EOF in server + res = c.read + assert_match %r{\AHTTP/1\.1 400 }, res + end + } + end + + def test_big_chunks + nr_out = 3 + buf = 'big' # 3 bytes is bigger than 2! + config = { :InputBufferSize => 2 }.freeze + total = 0 + all = '' + TestWEBrick.start_httpserver(config){|server, addr, port, log| + server.mount_proc('/', ->(req, res) { + err = [] + ret = req.body do |chunk| + n = chunk.bytesize + n > config[:InputBufferSize] and err << "#{n} > :InputBufferSize" + total += n + all << chunk + end + ret.nil? or err << 'req.body should return nil' + (buf * nr_out) == all or err << 'input body does not match expected' + res.header['connection'] = 'close' + res.body = err.join("\n") + }) + TCPSocket.open(addr, port) do |c| + c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \ + "Transfer-Encoding: chunked\r\n\r\n") + chunk = "#{buf.bytesize.to_s(16)}\r\n#{buf}\r\n" + nr_out.times { c.write(chunk) } + c.write("0\r\n\r\n") + head, body = c.read.split("\r\n\r\n") + assert_match %r{\AHTTP/1\.1 200 OK}, head + assert_nil body + end + } + end +end diff --git a/tool/test/webrick/test_httpstatus.rb b/tool/test/webrick/test_httpstatus.rb new file mode 100644 index 0000000000..fd0570d5c6 --- /dev/null +++ b/tool/test/webrick/test_httpstatus.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick" + +class TestWEBrickHTTPStatus < Test::Unit::TestCase + def test_info? + assert WEBrick::HTTPStatus.info?(100) + refute WEBrick::HTTPStatus.info?(200) + end + + def test_success? + assert WEBrick::HTTPStatus.success?(200) + refute WEBrick::HTTPStatus.success?(300) + end + + def test_redirect? + assert WEBrick::HTTPStatus.redirect?(300) + refute WEBrick::HTTPStatus.redirect?(400) + end + + def test_error? + assert WEBrick::HTTPStatus.error?(400) + refute WEBrick::HTTPStatus.error?(600) + end + + def test_client_error? + assert WEBrick::HTTPStatus.client_error?(400) + refute WEBrick::HTTPStatus.client_error?(500) + end + + def test_server_error? + assert WEBrick::HTTPStatus.server_error?(500) + refute WEBrick::HTTPStatus.server_error?(600) + end +end diff --git a/tool/test/webrick/test_httputils.rb b/tool/test/webrick/test_httputils.rb new file mode 100644 index 0000000000..00f297bd09 --- /dev/null +++ b/tool/test/webrick/test_httputils.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/httputils" + +class TestWEBrickHTTPUtils < Test::Unit::TestCase + include WEBrick::HTTPUtils + + def test_normilize_path + assert_equal("/foo", normalize_path("/foo")) + assert_equal("/foo/bar/", normalize_path("/foo/bar/")) + + assert_equal("/", normalize_path("/foo/../")) + assert_equal("/", normalize_path("/foo/..")) + assert_equal("/", normalize_path("/foo/bar/../../")) + assert_equal("/", normalize_path("/foo/bar/../..")) + assert_equal("/", normalize_path("/foo/bar/../..")) + assert_equal("/baz", normalize_path("/foo/bar/../../baz")) + assert_equal("/baz", normalize_path("/foo/../bar/../baz")) + assert_equal("/baz/", normalize_path("/foo/../bar/../baz/")) + assert_equal("/...", normalize_path("/bar/../...")) + assert_equal("/.../", normalize_path("/bar/../.../")) + + assert_equal("/foo/", normalize_path("/foo/./")) + assert_equal("/foo/", normalize_path("/foo/.")) + assert_equal("/foo/", normalize_path("/foo/././")) + assert_equal("/foo/", normalize_path("/foo/./.")) + assert_equal("/foo/bar", normalize_path("/foo/./bar")) + assert_equal("/foo/bar/", normalize_path("/foo/./bar/.")) + assert_equal("/foo/bar/", normalize_path("/./././foo/./bar/.")) + + assert_equal("/foo/bar/", normalize_path("//foo///.//bar/.///.//")) + assert_equal("/", normalize_path("//foo///..///bar/.///..//.//")) + + assert_raise(RuntimeError){ normalize_path("foo/bar") } + assert_raise(RuntimeError){ normalize_path("..") } + assert_raise(RuntimeError){ normalize_path("/..") } + assert_raise(RuntimeError){ normalize_path("/./..") } + assert_raise(RuntimeError){ normalize_path("/./../") } + assert_raise(RuntimeError){ normalize_path("/./../..") } + assert_raise(RuntimeError){ normalize_path("/./../../") } + assert_raise(RuntimeError){ normalize_path("/./../") } + assert_raise(RuntimeError){ normalize_path("/../..") } + assert_raise(RuntimeError){ normalize_path("/../../") } + assert_raise(RuntimeError){ normalize_path("/../../..") } + assert_raise(RuntimeError){ normalize_path("/../../../") } + assert_raise(RuntimeError){ normalize_path("/../foo/../") } + assert_raise(RuntimeError){ normalize_path("/../foo/../../") } + assert_raise(RuntimeError){ normalize_path("/foo/bar/../../../../") } + assert_raise(RuntimeError){ normalize_path("/foo/../bar/../../") } + assert_raise(RuntimeError){ normalize_path("/./../bar/") } + assert_raise(RuntimeError){ normalize_path("/./../") } + end + + def test_split_header_value + assert_equal(['foo', 'bar'], split_header_value('foo, bar')) + assert_equal(['"foo"', 'bar'], split_header_value('"foo", bar')) + assert_equal(['foo', '"bar"'], split_header_value('foo, "bar"')) + assert_equal(['*'], split_header_value('*')) + assert_equal(['W/"xyzzy"', 'W/"r2d2xxxx"', 'W/"c3piozzzz"'], + split_header_value('W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"')) + end + + def test_escape + assert_equal("/foo/bar", escape("/foo/bar")) + assert_equal("/~foo/bar", escape("/~foo/bar")) + assert_equal("/~foo%20bar", escape("/~foo bar")) + assert_equal("/~foo%20bar", escape("/~foo bar")) + assert_equal("/~foo%09bar", escape("/~foo\tbar")) + assert_equal("/~foo+bar", escape("/~foo+bar")) + bug8425 = '[Bug #8425] [ruby-core:55052]' + assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) { + assert_equal("%E3%83%AB%E3%83%93%E3%83%BC%E3%81%95%E3%82%93", escape("\u{30EB 30D3 30FC 3055 3093}")) + } + end + + def test_escape_form + assert_equal("%2Ffoo%2Fbar", escape_form("/foo/bar")) + assert_equal("%2F~foo%2Fbar", escape_form("/~foo/bar")) + assert_equal("%2F~foo+bar", escape_form("/~foo bar")) + assert_equal("%2F~foo+%2B+bar", escape_form("/~foo + bar")) + end + + def test_unescape + assert_equal("/foo/bar", unescape("%2ffoo%2fbar")) + assert_equal("/~foo/bar", unescape("/%7efoo/bar")) + assert_equal("/~foo/bar", unescape("%2f%7efoo%2fbar")) + assert_equal("/~foo+bar", unescape("/%7efoo+bar")) + end + + def test_unescape_form + assert_equal("//foo/bar", unescape_form("/%2Ffoo/bar")) + assert_equal("//foo/bar baz", unescape_form("/%2Ffoo/bar+baz")) + assert_equal("/~foo/bar baz", unescape_form("/%7Efoo/bar+baz")) + end + + def test_escape_path + assert_equal("/foo/bar", escape_path("/foo/bar")) + assert_equal("/foo/bar/", escape_path("/foo/bar/")) + assert_equal("/%25foo/bar/", escape_path("/%foo/bar/")) + end +end diff --git a/tool/test/webrick/test_httpversion.rb b/tool/test/webrick/test_httpversion.rb new file mode 100644 index 0000000000..e50ee17971 --- /dev/null +++ b/tool/test/webrick/test_httpversion.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/httpversion" + +class TestWEBrickHTTPVersion < Test::Unit::TestCase + def setup + @v09 = WEBrick::HTTPVersion.new("0.9") + @v10 = WEBrick::HTTPVersion.new("1.0") + @v11 = WEBrick::HTTPVersion.new("1.001") + end + + def test_to_s() + assert_equal("0.9", @v09.to_s) + assert_equal("1.0", @v10.to_s) + assert_equal("1.1", @v11.to_s) + end + + def test_major() + assert_equal(0, @v09.major) + assert_equal(1, @v10.major) + assert_equal(1, @v11.major) + end + + def test_minor() + assert_equal(9, @v09.minor) + assert_equal(0, @v10.minor) + assert_equal(1, @v11.minor) + end + + def test_compar() + assert_equal(0, @v09 <=> "0.9") + assert_equal(0, @v09 <=> "0.09") + + assert_equal(-1, @v09 <=> @v10) + assert_equal(-1, @v09 <=> "1.00") + + assert_equal(1, @v11 <=> @v09) + assert_equal(1, @v11 <=> "1.0") + assert_equal(1, @v11 <=> "0.9") + end +end diff --git a/tool/test/webrick/test_server.rb b/tool/test/webrick/test_server.rb new file mode 100644 index 0000000000..815cc3ce39 --- /dev/null +++ b/tool/test/webrick/test_server.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: false +require "test/unit" +require "tempfile" +require "webrick" +require_relative "utils" + +class TestWEBrickServer < Test::Unit::TestCase + class Echo < WEBrick::GenericServer + def run(sock) + while line = sock.gets + sock << line + end + end + end + + def test_server + TestWEBrick.start_server(Echo){|server, addr, port, log| + TCPSocket.open(addr, port){|sock| + sock.puts("foo"); assert_equal("foo\n", sock.gets, log.call) + sock.puts("bar"); assert_equal("bar\n", sock.gets, log.call) + sock.puts("baz"); assert_equal("baz\n", sock.gets, log.call) + sock.puts("qux"); assert_equal("qux\n", sock.gets, log.call) + } + } + end + + def test_start_exception + stopped = 0 + + log = [] + logger = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) + + assert_raise(SignalException) do + listener = Object.new + def listener.to_io # IO.select invokes #to_io. + raise SignalException, 'SIGTERM' # simulate signal in main thread + end + def listener.shutdown + end + def listener.close + end + + server = WEBrick::HTTPServer.new({ + :BindAddress => "127.0.0.1", :Port => 0, + :StopCallback => Proc.new{ stopped += 1 }, + :Logger => logger, + }) + server.listeners[0].close + server.listeners[0] = listener + + server.start + end + + assert_equal(1, stopped) + assert_equal(1, log.length) + assert_match(/FATAL SignalException: SIGTERM/, log[0]) + end + + def test_callbacks + accepted = started = stopped = 0 + config = { + :AcceptCallback => Proc.new{ accepted += 1 }, + :StartCallback => Proc.new{ started += 1 }, + :StopCallback => Proc.new{ stopped += 1 }, + } + TestWEBrick.start_server(Echo, config){|server, addr, port, log| + true while server.status != :Running + sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait + assert_equal(1, started, log.call) + assert_equal(0, stopped, log.call) + assert_equal(0, accepted, log.call) + TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } + TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } + TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } + assert_equal(3, accepted, log.call) + } + assert_equal(1, started) + assert_equal(1, stopped) + end + + def test_daemon + begin + r, w = IO.pipe + pid1 = Process.fork{ + r.close + WEBrick::Daemon.start + w.puts(Process.pid) + sleep 10 + } + pid2 = r.gets.to_i + assert(Process.kill(:KILL, pid2)) + assert_not_equal(pid1, pid2) + rescue NotImplementedError + # snip this test + ensure + Process.wait(pid1) if pid1 + r.close + w.close + end + end + + def test_restart_after_shutdown + address = '127.0.0.1' + port = 0 + log = [] + config = { + :BindAddress => address, + :Port => port, + :Logger => WEBrick::Log.new(log, WEBrick::BasicLog::WARN), + } + server = Echo.new(config) + client_proc = lambda {|str| + begin + ret = server.listeners.first.connect_address.connect {|s| + s.write(str) + s.close_write + s.read + } + assert_equal(str, ret) + ensure + server.shutdown + end + } + server_thread = Thread.new { server.start } + client_thread = Thread.new { client_proc.call("a") } + assert_join_threads([client_thread, server_thread]) + server.listen(address, port) + server_thread = Thread.new { server.start } + client_thread = Thread.new { client_proc.call("b") } + assert_join_threads([client_thread, server_thread]) + assert_equal([], log) + end + + def test_restart_after_stop + log = Object.new + class << log + include Test::Unit::Assertions + def <<(msg) + flunk "unexpected log: #{msg.inspect}" + end + end + client_thread = nil + wakeup = -> {client_thread.wakeup} + warn_flunk = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) + server = WEBrick::HTTPServer.new( + :StartCallback => wakeup, + :StopCallback => wakeup, + :BindAddress => '0.0.0.0', + :Port => 0, + :Logger => warn_flunk) + 2.times { + server_thread = Thread.start { + server.start + } + client_thread = Thread.start { + sleep 0.1 until server.status == :Running || !server_thread.status + server.stop + sleep 0.1 until server.status == :Stop || !server_thread.status + } + assert_join_threads([client_thread, server_thread]) + } + end + + def test_port_numbers + config = { + :BindAddress => '0.0.0.0', + :Logger => WEBrick::Log.new([], WEBrick::BasicLog::WARN), + } + + ports = [0, "0"] + + ports.each do |port| + config[:Port]= port + server = WEBrick::GenericServer.new(config) + server_thread = Thread.start { server.start } + client_thread = Thread.start { + sleep 0.1 until server.status == :Running || !server_thread.status + server_port = server.listeners[0].addr[1] + server.stop + assert_equal server.config[:Port], server_port + sleep 0.1 until server.status == :Stop || !server_thread.status + } + assert_join_threads([client_thread, server_thread]) + end + + assert_raise(ArgumentError) do + config[:Port]= "FOO" + WEBrick::GenericServer.new(config) + end + end +end diff --git a/tool/test/webrick/test_ssl_server.rb b/tool/test/webrick/test_ssl_server.rb new file mode 100644 index 0000000000..4e52598bf5 --- /dev/null +++ b/tool/test/webrick/test_ssl_server.rb @@ -0,0 +1,67 @@ +require "test/unit" +require "webrick" +require "webrick/ssl" +require_relative "utils" +require 'timeout' + +class TestWEBrickSSLServer < Test::Unit::TestCase + class Echo < WEBrick::GenericServer + def run(sock) + while line = sock.gets + sock << line + end + end + end + + def test_self_signed_cert_server + assert_self_signed_cert( + :SSLEnable => true, + :SSLCertName => [["C", "JP"], ["O", "www.ruby-lang.org"], ["CN", "Ruby"]], + ) + end + + def test_self_signed_cert_server_with_string + assert_self_signed_cert( + :SSLEnable => true, + :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", + ) + end + + def assert_self_signed_cert(config) + TestWEBrick.start_server(Echo, config){|server, addr, port, log| + io = TCPSocket.new(addr, port) + sock = OpenSSL::SSL::SSLSocket.new(io) + sock.connect + sock.puts(server.ssl_context.cert.subject.to_s) + assert_equal("/C=JP/O=www.ruby-lang.org/CN=Ruby\n", sock.gets, log.call) + sock.close + io.close + } + end + + def test_slow_connect + poke = lambda do |io, msg| + begin + sock = OpenSSL::SSL::SSLSocket.new(io) + sock.connect + sock.puts(msg) + assert_equal "#{msg}\n", sock.gets, msg + ensure + sock&.close + io.close + end + end + config = { + :SSLEnable => true, + :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", + } + EnvUtil.timeout(10) do + TestWEBrick.start_server(Echo, config) do |server, addr, port, log| + outer = TCPSocket.new(addr, port) + inner = TCPSocket.new(addr, port) + poke.call(inner, 'fast TLS negotiation') + poke.call(outer, 'slow TLS negotiation') + end + end + end +end diff --git a/tool/test/webrick/test_utils.rb b/tool/test/webrick/test_utils.rb new file mode 100644 index 0000000000..c2b7a36e8a --- /dev/null +++ b/tool/test/webrick/test_utils.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: false +require "test/unit" +require "webrick/utils" + +class TestWEBrickUtils < Test::Unit::TestCase + def teardown + WEBrick::Utils::TimeoutHandler.terminate + super + end + + def assert_expired(m) + Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do + assert_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) + end + end + + def assert_not_expired(m) + Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do + assert_not_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) + end + end + + EX = Class.new(StandardError) + + def test_no_timeout + m = WEBrick::Utils + assert_equal(:foo, m.timeout(10){ :foo }) + assert_expired(m) + end + + def test_nested_timeout_outer + m = WEBrick::Utils + i = 0 + assert_raise(Timeout::Error){ + m.timeout(1){ + assert_raise(Timeout::Error){ m.timeout(0.1){ i += 1; sleep(1) } } + assert_not_expired(m) + i += 1 + sleep(2) + } + } + assert_equal(2, i) + assert_expired(m) + end + + def test_timeout_default_exception + m = WEBrick::Utils + assert_raise(Timeout::Error){ m.timeout(0.01){ sleep } } + assert_expired(m) + end + + def test_timeout_custom_exception + m = WEBrick::Utils + ex = EX + assert_raise(ex){ m.timeout(0.01, ex){ sleep } } + assert_expired(m) + end + + def test_nested_timeout_inner_custom_exception + m = WEBrick::Utils + ex = EX + i = 0 + assert_raise(ex){ + m.timeout(10){ + m.timeout(0.01, ex){ i += 1; sleep } + } + sleep + } + assert_equal(1, i) + assert_expired(m) + end + + def test_nested_timeout_outer_custom_exception + m = WEBrick::Utils + ex = EX + i = 0 + assert_raise(Timeout::Error){ + m.timeout(0.01){ + m.timeout(1.0, ex){ i += 1; sleep } + } + sleep + } + assert_equal(1, i) + assert_expired(m) + end + + def test_create_listeners + addr = listener_address(0) + port = addr.slice!(1) + assert_kind_of(Integer, port, "dynamically chosen port number") + assert_equal(["AF_INET", "127.0.0.1", "127.0.0.1"], addr) + + assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], + listener_address(port), + "specific port number") + + assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], + listener_address(port.to_s), + "specific port number string") + end + + def listener_address(port) + listeners = WEBrick::Utils.create_listeners("127.0.0.1", port) + srv = listeners.first + assert_kind_of TCPServer, srv + srv.addr + ensure + listeners.each(&:close) if listeners + end +end diff --git a/tool/test/webrick/utils.rb b/tool/test/webrick/utils.rb new file mode 100644 index 0000000000..56d3a30ea4 --- /dev/null +++ b/tool/test/webrick/utils.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: false +require "webrick" +begin + require "webrick/https" +rescue LoadError +end +require "webrick/httpproxy" + +module TestWEBrick + NullWriter = Object.new + def NullWriter.<<(msg) + puts msg if $DEBUG + return self + end + + class WEBrick::HTTPServlet::CGIHandler + remove_const :Ruby + require "envutil" unless defined?(EnvUtil) + Ruby = EnvUtil.rubybin + remove_const :CGIRunner + CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: + remove_const :CGIRunnerArray + CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb"] # :nodoc: + end + + RubyBin = "\"#{EnvUtil.rubybin}\"" + RubyBin << " --disable-gems" + RubyBin << " \"-I#{File.expand_path("../..", File.dirname(__FILE__))}/lib\"" + RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/common\"" + RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}\"" + + RubyBinArray = [EnvUtil.rubybin] + RubyBinArray << "--disable-gems" + RubyBinArray << "-I" << "#{File.expand_path("../..", File.dirname(__FILE__))}/lib" + RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/common" + RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}" + + require "test/unit" unless defined?(Test::Unit) + include Test::Unit::Assertions + extend Test::Unit::Assertions + + module_function + + DefaultLogTester = lambda {|log, access_log| assert_equal([], log) } + + def start_server(klass, config={}, log_tester=DefaultLogTester, &block) + log_ary = [] + access_log_ary = [] + log = proc { "webrick log start:\n" + (log_ary+access_log_ary).join.gsub(/^/, " ").chomp + "\nwebrick log end" } + config = ({ + :BindAddress => "127.0.0.1", :Port => 0, + :ServerType => Thread, + :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN), + :AccessLog => [[access_log_ary, ""]] + }.update(config)) + server = capture_output {break klass.new(config)} + server_thread = server.start + server_thread2 = Thread.new { + server_thread.join + if log_tester + log_tester.call(log_ary, access_log_ary) + end + } + addr = server.listeners[0].addr + client_thread = Thread.new { + begin + block.yield([server, addr[3], addr[1], log]) + ensure + server.shutdown + end + } + assert_join_threads([client_thread, server_thread2]) + end + + def start_httpserver(config={}, log_tester=DefaultLogTester, &block) + start_server(WEBrick::HTTPServer, config, log_tester, &block) + end + + def start_httpproxy(config={}, log_tester=DefaultLogTester, &block) + start_server(WEBrick::HTTPProxyServer, config, log_tester, &block) + end +end diff --git a/tool/test/webrick/webrick.cgi b/tool/test/webrick/webrick.cgi new file mode 100644 index 0000000000..a294fa72f9 --- /dev/null +++ b/tool/test/webrick/webrick.cgi @@ -0,0 +1,38 @@ +#!ruby +require "webrick/cgi" + +class TestApp < WEBrick::CGI + def do_GET(req, res) + res["content-type"] = "text/plain" + if req.path_info == "/dumpenv" + res.body = Marshal.dump(ENV.to_hash) + elsif (p = req.path_info) && p.length > 0 + res.body = p + elsif (q = req.query).size > 0 + res.body = q.keys.sort.collect{|key| + q[key].list.sort.collect{|v| + "#{key}=#{v}" + }.join(", ") + }.join(", ") + elsif %r{/$} =~ req.request_uri.to_s + res.body = "" + res.body << req.request_uri.to_s << "\n" + res.body << req.script_name + elsif !req.cookies.empty? + res.body = req.cookies.inject(""){|result, cookie| + result << "%s=%s\n" % [cookie.name, cookie.value] + } + res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") + res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") + else + res.body = req.script_name + end + end + + def do_POST(req, res) + do_GET(req, res) + end +end + +cgi = TestApp.new +cgi.start diff --git a/tool/test/webrick/webrick.rhtml b/tool/test/webrick/webrick.rhtml new file mode 100644 index 0000000000..a7bbe43fb5 --- /dev/null +++ b/tool/test/webrick/webrick.rhtml @@ -0,0 +1,4 @@ +req to <%= +servlet_request.request_uri +%> <%= +servlet_request.query.inspect %> diff --git a/tool/test/webrick/webrick_long_filename.cgi b/tool/test/webrick/webrick_long_filename.cgi new file mode 100644 index 0000000000..43c1af825c --- /dev/null +++ b/tool/test/webrick/webrick_long_filename.cgi @@ -0,0 +1,36 @@ +#!ruby +require "webrick/cgi" + +class TestApp < WEBrick::CGI + def do_GET(req, res) + res["content-type"] = "text/plain" + if (p = req.path_info) && p.length > 0 + res.body = p + elsif (q = req.query).size > 0 + res.body = q.keys.sort.collect{|key| + q[key].list.sort.collect{|v| + "#{key}=#{v}" + }.join(", ") + }.join(", ") + elsif %r{/$} =~ req.request_uri.to_s + res.body = "" + res.body << req.request_uri.to_s << "\n" + res.body << req.script_name + elsif !req.cookies.empty? + res.body = req.cookies.inject(""){|result, cookie| + result << "%s=%s\n" % [cookie.name, cookie.value] + } + res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") + res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") + else + res.body = req.script_name + end + end + + def do_POST(req, res) + do_GET(req, res) + end +end + +cgi = TestApp.new +cgi.start |