diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0021fa89..edede5b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,15 +3,30 @@ name: test on: [push, pull_request] jobs: - build: + ruby-versions: + uses: ruby/actions/.github/workflows/ruby_versions.yml@master + with: + engine: cruby + min_version: 2.4 + + test: + needs: ruby-versions name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: + fail-fast: false matrix: - ruby: [ "3.0", 2.7, 2.6, 2.5, 2.4, head ] + ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} os: [ ubuntu-latest, macos-latest ] + # CRuby < 2.6 does not support macos-arm64, so test those on amd64 instead + exclude: + - { os: macos-latest, ruby: '2.4' } + - { os: macos-latest, ruby: '2.5' } + include: + - { os: macos-13, ruby: '2.4' } + - { os: macos-13, ruby: '2.5' } runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/Gemfile b/Gemfile index 10284be2..f5b6c4b6 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ gemspec gem "rake" gem "test-unit" +gem "test-unit-ruby-core" diff --git a/Rakefile b/Rakefile index 5a7afabd..5d512c89 100644 --- a/Rakefile +++ b/Rakefile @@ -7,11 +7,4 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList["test/**/test_*.rb"] end -task :sync_tool do - require 'fileutils' - FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" - FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" - FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" -end - task :default => :test diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb index 680ac65a..1fa5dbb4 100644 --- a/lib/webrick/httprequest.rb +++ b/lib/webrick/httprequest.rb @@ -318,7 +318,7 @@ def content_type def [](header_name) if @header value = @header[header_name.downcase] - value.empty? ? nil : value.join(", ") + value.empty? ? nil : value.join end end @@ -329,7 +329,7 @@ def each if @header @header.each{|k, v| value = @header[k] - yield(k, value.empty? ? nil : value.join(", ")) + yield(k, value.empty? ? nil : value.join) } end end @@ -402,7 +402,7 @@ def fixup() # :nodoc: # This method provides the metavariables defined by the revision 3 # of "The WWW Common Gateway Interface Version 1.1" # To browse the current document of CGI Version 1.1, see below: - # http://tools.ietf.org/html/rfc3875 + # https://www.rfc-editor.org/rfc/rfc3875 def meta_vars meta = Hash.new @@ -458,7 +458,7 @@ def read_request_line(socket) end @request_time = Time.now - if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line + if /^(\S+) (\S++)(?: HTTP\/(\d+\.\d+))?\r\n/mo =~ @request_line @request_method = $1 @unparsed_uri = $2 @http_version = HTTPVersion.new($3 ? $3 : "0.9") @@ -470,15 +470,34 @@ def read_request_line(socket) def read_header(socket) if socket + end_of_headers = false + while line = read_line(socket) - break if /\A(#{CRLF}|#{LF})\z/om =~ line + if line == CRLF + end_of_headers = true + break + end if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH raise HTTPStatus::RequestEntityTooLarge, 'headers too large' end + if line.include?("\x00") + raise HTTPStatus::BadRequest, 'null byte in header' + end @raw_header << line end + + # Allow if @header already set to support chunked trailers + raise HTTPStatus::EOFError unless end_of_headers || @header end @header = HTTPUtils::parse_header(@raw_header.join) + + if (content_length = @header['content-length']) && content_length.length != 0 + if content_length.length > 1 + raise HTTPStatus::BadRequest, "multiple content-length request headers" + elsif !/\A\d+\z/.match?(content_length[0]) + raise HTTPStatus::BadRequest, "invalid content-length request header" + end + end end def parse_uri(str, scheme="http") @@ -503,14 +522,19 @@ def parse_uri(str, scheme="http") return URI::parse(uri.to_s) end + host_pattern = URI::RFC2396_Parser.new.pattern.fetch(:HOST) + HOST_PATTERN = /\A(#{host_pattern})(?::(\d+))?\z/n def parse_host_request_line(host) - pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/no - host.scan(pattern)[0] + host.scan(HOST_PATTERN)[0] end def read_body(socket, block) return unless socket if tc = self['transfer-encoding'] + if self['content-length'] + raise HTTPStatus::BadRequest, "request with both transfer-encoding and content-length, possible request smuggling" + end + case tc when /\Achunked\z/io then read_chunked(socket, block) else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}." @@ -534,7 +558,7 @@ def read_body(socket, block) def read_chunk_size(socket) line = read_line(socket) - if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line + if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r\n\z/ =~ line chunk_size = $1.hex chunk_ext = $2 [ chunk_size, chunk_ext ] @@ -555,7 +579,11 @@ def read_chunked(socket, block) block.call(data) end while (chunk_size -= sz) > 0 - read_line(socket) # skip CRLF + line = read_line(socket) # skip CRLF + unless line == "\r\n" + raise HTTPStatus::BadRequest, "extra data after chunk `#{line}'." + end + chunk_size, = read_chunk_size(socket) end read_header(socket) # trailer + CRLF diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb index 48aa1371..92f3044d 100644 --- a/lib/webrick/httputils.rb +++ b/lib/webrick/httputils.rb @@ -55,7 +55,6 @@ def normalize_path(path) "cer" => "application/pkix-cert", "crl" => "application/pkix-crl", "crt" => "application/x-x509-ca-cert", - #"crl" => "application/x-pkcs7-crl", "css" => "text/css", "dms" => "application/octet-stream", "doc" => "application/msword", @@ -153,28 +152,49 @@ def mime_type(filename, mime_tab) # Parses an HTTP header +raw+ into a hash of header fields with an Array # of values. + class SplitHeader < Array + def join(separator = ", ") + super + end + end + + class CookieHeader < Array + def join(separator = "; ") + super + end + end + + HEADER_CLASSES = Hash.new(SplitHeader).update({ + "cookie" => CookieHeader, + }) + def parse_header(raw) header = Hash.new([].freeze) field = nil raw.each_line{|line| case line - when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om + when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om field, value = $1, $2 field.downcase! - header[field] = [] unless header.has_key?(field) + header[field] = HEADER_CLASSES[field].new unless header.has_key?(field) header[field] << value - when /^\s+(.*?)\s*\z/om - value = $1 + when /^[ \t]+([^\r\n\0]*?)\r\n/om unless field raise HTTPStatus::BadRequest, "bad header '#{line}'." end + value = line + value.gsub!(/\A[ \t]+/, '') + value.slice!(-2..-1) header[field][-1] << " " << value else raise HTTPStatus::BadRequest, "bad header '#{line}'." end } header.each{|key, values| - values.each(&:strip!) + values.each{|value| + value.gsub!(/\A[ \t]+/, '') + value.gsub!(/[ \t]+\z/, '') + } } header end @@ -184,8 +204,8 @@ def parse_header(raw) # Splits a header value +str+ according to HTTP specification. def split_header_value(str) - str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) - (?:,\s*|\Z)'xn).flatten + str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+) + (?:,[ \t]*|\Z)'xn).flatten end module_function :split_header_value @@ -213,9 +233,9 @@ def parse_range_header(ranges_specifier) def parse_qvalues(value) tmp = [] if value - parts = value.split(/,\s*/) + parts = value.split(/,[ \t]*/) parts.each {|part| - if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) + if m = %r{^([^ \t,]+?)(?:;[ \t]*q=(\d+(?:\.\d+)?))?$}.match(part) val = m[1] q = (m[2] or 1).to_f tmp.push([val, q]) @@ -314,8 +334,8 @@ def <<(str) elsif str == CRLF @header = HTTPUtils::parse_header(@raw_header.join) if cd = self['content-disposition'] - if /\s+name="(.*?)"/ =~ cd then @name = $1 end - if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end + if /[ \t]+name="(.*?)"/ =~ cd then @name = $1 end + if /[ \t]+filename="(.*?)"/ =~ cd then @filename = $1 end end else @raw_header << str diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb index ceeefc33..fbea0dda 100644 --- a/lib/webrick/version.rb +++ b/lib/webrick/version.rb @@ -14,5 +14,5 @@ module WEBrick ## # The WEBrick version - VERSION = "1.8.1" + VERSION = "1.8.2" end diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb deleted file mode 100644 index bac3856a..00000000 --- a/test/lib/core_assertions.rb +++ /dev/null @@ -1,768 +0,0 @@ -# frozen_string_literal: true - -module Test - module Unit - module Assertions - def _assertions= n # :nodoc: - @_assertions = n - end - - def _assertions # :nodoc: - @_assertions ||= 0 - end - - ## - # Returns a proc that will output +msg+ along with the default message. - - def message msg = nil, ending = nil, &default - proc { - msg = msg.call.chomp(".") if Proc === msg - custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? - "#{custom_message}#{default.call}#{ending || "."}" - } - end - end - - module CoreAssertions - require_relative 'envutil' - require 'pp' - - def mu_pp(obj) #:nodoc: - obj.pretty_inspect.chomp - end - - def assert_file - AssertFile - end - - FailDesc = proc do |status, message = "", out = ""| - now = Time.now - proc do - EnvUtil.failure_description(status, now, message, out) - end - end - - def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, - success: nil, **opt) - args = Array(args).dup - args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') - stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) - desc = FailDesc[status, message, stderr] - if block_given? - raise "test_stdout ignored, use block only or without block" if test_stdout != [] - raise "test_stderr ignored, use block only or without block" if test_stderr != [] - yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) - else - all_assertions(desc) do |a| - [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| - a.for(key) do - if exp.is_a?(Regexp) - assert_match(exp, act) - elsif exp.all? {|e| String === e} - assert_equal(exp, act.lines.map {|l| l.chomp }) - else - assert_pattern_list(exp, act) - end - end - end - unless success.nil? - a.for("success?") do - if success - assert_predicate(status, :success?) - else - assert_not_predicate(status, :success?) - end - end - end - end - status - end - end - - if defined?(RubyVM::InstructionSequence) - def syntax_check(code, fname, line) - code = code.dup.force_encoding(Encoding::UTF_8) - RubyVM::InstructionSequence.compile(code, fname, fname, line) - :ok - ensure - raise if SyntaxError === $! - end - else - def syntax_check(code, fname, line) - code = code.b - code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { - "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" - } - code = code.force_encoding(Encoding::UTF_8) - catch {|tag| eval(code, binding, fname, line - 1)} - end - end - - def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) - # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail - pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - - require_relative 'memory_status' - raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) - - token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" - token_dump = token.dump - token_re = Regexp.quote(token) - envs = args.shift if Array === args and Hash === args.first - args = [ - "--disable=gems", - "-r", File.expand_path("../memory_status", __FILE__), - *args, - "-v", "-", - ] - if defined? Memory::NO_MEMORY_LEAK_ENVS then - envs ||= {} - newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } - envs = newenvs if newenvs - end - args.unshift(envs) if envs - cmd = [ - 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', - prepare, - 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', - '$initial_size = $initial_status.size', - code, - 'GC.start', - ].join("\n") - _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) - before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) - after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) - assert(status.success?, FailDesc[status, message, err]) - ([:size, (rss && :rss)] & after.members).each do |n| - b = before[n] - a = after[n] - next unless a > 0 and b > 0 - assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) - end - rescue LoadError - pend - end - - # :call-seq: - # assert_nothing_raised( *args, &block ) - # - #If any exceptions are given as arguments, the assertion will - #fail if one of those exceptions are raised. Otherwise, the test fails - #if any exceptions are raised. - # - #The final argument may be a failure message. - # - # assert_nothing_raised RuntimeError do - # raise Exception #Assertion passes, Exception is not a RuntimeError - # end - # - # assert_nothing_raised do - # raise Exception #Assertion fails - # end - def assert_nothing_raised(*args) - self._assertions += 1 - if Module === args.last - msg = nil - else - msg = args.pop - end - begin - line = __LINE__; yield - rescue Test::Unit::PendedError - raise - rescue Exception => e - bt = e.backtrace - as = e.instance_of?(Test::Unit::AssertionFailedError) - if as - ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o - bt.reject! {|ln| ans =~ ln} - end - if ((args.empty? && !as) || - args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) - msg = message(msg) { - "Exception raised:\n<#{mu_pp(e)}>\n" + - "Backtrace:\n" + - e.backtrace.map{|frame| " #{frame}"}.join("\n") - } - raise Test::Unit::AssertionFailedError, msg.call, bt - else - raise - end - end - end - - def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) - fname ||= caller_locations(2, 1)[0] - mesg ||= fname.to_s - verbose, $VERBOSE = $VERBOSE, verbose - case - when Array === fname - fname, line = *fname - when defined?(fname.path) && defined?(fname.lineno) - fname, line = fname.path, fname.lineno - else - line = 1 - end - yield(code, fname, line, message(mesg) { - if code.end_with?("\n") - "```\n#{code}```\n" - else - "```\n#{code}\n```\n""no-newline" - end - }) - ensure - $VERBOSE = verbose - end - - def assert_valid_syntax(code, *args, **opt) - prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| - yield if defined?(yield) - assert_nothing_raised(SyntaxError, mesg) do - assert_equal(:ok, syntax_check(src, fname, line), mesg) - end - end - end - - def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) - assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) - if child_env - child_env = [child_env] - else - child_env = [] - end - out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) - assert !status.signaled?, FailDesc[status, message, out] - end - - def assert_ruby_status(args, test_stdin="", message=nil, **opt) - out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) - desc = FailDesc[status, message, out] - assert(!status.signaled?, desc) - message ||= "ruby exit status is not success:" - assert(status.success?, desc) - end - - ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") - - def separated_runner(out = nil) - include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) - out = out ? IO.new(out, 'w') : STDOUT - at_exit { - out.puts [Marshal.dump($!)].pack('m'), "assertions=#{self._assertions}" - } - Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) - end - - def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) - unless file and line - loc, = caller_locations(1,1) - file ||= loc.path - line ||= loc.lineno - end - capture_stdout = true - unless /mswin|mingw/ =~ RUBY_PLATFORM - capture_stdout = false - opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) - res_p, res_c = IO.pipe - opt[:ios] = [res_c] - end - src = < marshal_error - ignore_stderr = nil - res = nil - end - if res and !(SystemExit === res) - if bt = res.backtrace - bt.each do |l| - l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} - end - bt.concat(caller) - else - res.set_backtrace(caller) - end - raise res - end - - # really is it succeed? - unless ignore_stderr - # the body of assert_separately must not output anything to detect error - assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) - end - assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) - raise marshal_error if marshal_error - end - - # Run Ractor-related test without influencing the main test suite - def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) - return unless defined?(Ractor) - - require = "require #{require.inspect}" if require - if require_relative - dir = File.dirname(caller_locations[0,1][0].absolute_path) - full_path = File.expand_path(require_relative, dir) - require = "#{require}; require #{full_path.inspect}" - end - - assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) - #{require} - previous_verbose = $VERBOSE - $VERBOSE = nil - Ractor.new {} # trigger initial warning - $VERBOSE = previous_verbose - #{src} - RUBY - end - - # :call-seq: - # assert_throw( tag, failure_message = nil, &block ) - # - #Fails unless the given block throws +tag+, returns the caught - #value otherwise. - # - #An optional failure message may be provided as the final argument. - # - # tag = Object.new - # assert_throw(tag, "#{tag} was not thrown!") do - # throw tag - # end - def assert_throw(tag, msg = nil) - ret = catch(tag) do - begin - yield(tag) - rescue UncaughtThrowError => e - thrown = e.tag - end - msg = message(msg) { - "Expected #{mu_pp(tag)} to have been thrown"\ - "#{%Q[, not #{thrown}] if thrown}" - } - assert(false, msg) - end - assert(true) - ret - end - - # :call-seq: - # assert_raise( *args, &block ) - # - #Tests if the given block raises an exception. Acceptable exception - #types may be given as optional arguments. If the last argument is a - #String, it will be used as the error message. - # - # assert_raise do #Fails, no Exceptions are raised - # end - # - # assert_raise NameError do - # puts x #Raises NameError, so assertion succeeds - # end - def assert_raise(*exp, &b) - case exp.last - when String, Proc - msg = exp.pop - end - - begin - yield - rescue Test::Unit::PendedError => e - return e if exp.include? Test::Unit::PendedError - raise e - rescue Exception => e - expected = exp.any? { |ex| - if ex.instance_of? Module then - e.kind_of? ex - else - e.instance_of? ex - end - } - - assert expected, proc { - flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) - } - - return e - ensure - unless e - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - end - end - - # :call-seq: - # assert_raise_with_message(exception, expected, msg = nil, &block) - # - #Tests if the given block raises an exception with the expected - #message. - # - # assert_raise_with_message(RuntimeError, "foo") do - # nil #Fails, no Exceptions are raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise ArgumentError, "foo" #Fails, different Exception is raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "bar" #Fails, RuntimeError is raised but the message differs - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "foo" #Raises RuntimeError with the message, so assertion succeeds - # end - def assert_raise_with_message(exception, expected, msg = nil, &block) - case expected - when String - assert = :assert_equal - when Regexp - assert = :assert_match - else - raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" - end - - ex = m = nil - EnvUtil.with_default_internal(expected.encoding) do - ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do - yield - end - m = ex.message - end - msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} - - if assert == :assert_equal - assert_equal(expected, m, msg) - else - msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } - assert expected =~ m, msg - block.binding.eval("proc{|_|$~=_}").call($~) - end - ex - end - - MINI_DIR = File.join(File.dirname(File.expand_path(__FILE__)), "minitest") #:nodoc: - - # :call-seq: - # assert(test, [failure_message]) - # - #Tests if +test+ is true. - # - #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used - #as the failure message. Otherwise, the result of calling +msg+ will be - #used as the message if the assertion fails. - # - #If no +msg+ is given, a default message will be used. - # - # assert(false, "This was expected to be true") - def assert(test, *msgs) - case msg = msgs.first - when String, Proc - when nil - msgs.shift - else - bt = caller.reject { |s| s.start_with?(MINI_DIR) } - raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt - end unless msgs.empty? - super - end - - # :call-seq: - # assert_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object responds to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_respond_to("hello", :reverse) #Succeeds - # assert_respond_to("hello", :does_not_exist) #Fails - def assert_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" - } - return assert obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return if obj.respond_to?(meth) - end - super(obj, meth, msg) - end - - # :call-seq: - # assert_not_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object does not respond to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_respond_to("hello", :reverse) #Fails - # assert_not_respond_to("hello", :does_not_exist) #Succeeds - def assert_not_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" - } - return assert !obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) - return unless obj.respond_to?(meth) - end - refute_respond_to(obj, meth, msg) - end - - # pattern_list is an array which contains regexp and :*. - # :* means any sequence. - # - # pattern_list is anchored. - # Use [:*, regexp, :*] for non-anchored match. - def assert_pattern_list(pattern_list, actual, message=nil) - rest = actual - anchored = true - pattern_list.each_with_index {|pattern, i| - if pattern == :* - anchored = false - else - if anchored - match = /\A#{pattern}/.match(rest) - else - match = pattern.match(rest) - end - unless match - msg = message(msg) { - expect_msg = "Expected #{mu_pp pattern}\n" - if /\n[^\n]/ =~ rest - actual_mesg = +"to match\n" - rest.scan(/.*\n+/) { - actual_mesg << ' ' << $&.inspect << "+\n" - } - actual_mesg.sub!(/\+\n\z/, '') - else - actual_mesg = "to match " + mu_pp(rest) - end - actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" - expect_msg + actual_mesg - } - assert false, msg - end - rest = match.post_match - anchored = true - end - } - if anchored - assert_equal("", rest) - end - end - - def assert_warning(pat, msg = nil) - result = nil - stderr = EnvUtil.with_default_internal(pat.encoding) { - EnvUtil.verbose_warning { - result = yield - } - } - msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - result - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} - end - - def assert_deprecated_warning(mesg = /deprecated/) - assert_warning(mesg) do - Warning[:deprecated] = true - yield - end - end - - def assert_deprecated_warn(mesg = /deprecated/) - assert_warn(mesg) do - Warning[:deprecated] = true - yield - end - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - include CoreAssertions - def assert_file_predicate(predicate, *args) - if /\Anot_/ =~ predicate - predicate = $' - neg = " not" - end - result = File.__send__(predicate, *args) - result = !result if neg - mesg = "Expected file ".dup << args.shift.inspect - mesg << "#{neg} to be #{predicate}" - mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? - mesg << " #{failure_message}" if failure_message - assert(result, mesg) - end - alias method_missing assert_file_predicate - - def for(message) - clone.tap {|a| a.failure_message = message} - end - end - - class AllFailures - attr_reader :failures - - def initialize - @count = 0 - @failures = {} - end - - def for(key) - @count += 1 - yield - rescue Exception => e - @failures[key] = [@count, e] - end - - def foreach(*keys) - keys.each do |key| - @count += 1 - begin - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - end - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - v = v.message - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - # threads should respond to shift method. - # Array can be used. - def assert_join_threads(threads, message = nil) - errs = [] - values = [] - while th = threads.shift - begin - values << th.value - rescue Exception - errs << [th, $!] - th = nil - end - end - values - ensure - if th&.alive? - th.raise(Timeout::Error.new) - th.join rescue errs << [th, $!] - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise Test::Unit::AssertionFailedError, msg - end - end - - def assert_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - unless blk.call(*a, &b) - failed << (a.size > 1 ? a : a[0]) - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - def assert_all_assertions(msg = nil) - all = AllFailures.new - yield all - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions assert_all_assertions - - def assert_all_assertions_foreach(msg = nil, *keys, &block) - all = AllFailures.new - all.foreach(*keys, &block) - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions_foreach assert_all_assertions_foreach - - def message(msg = nil, *args, &default) # :nodoc: - if Proc === msg - super(nil, *args) do - ary = [msg.call, (default.call if default)].compact.reject(&:empty?) - if 1 < ary.length - ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? 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| + TestWEBrick.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)} @@ -98,7 +76,7 @@ def test_bad_request log_tester = lambda {|log, access_log| assert_match(/BadRequest/, log.join) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) {|server, addr, port, log| sock = TCPSocket.new(addr, port) begin sock << "POST /webrick.cgi HTTP/1.0" << CRLF @@ -115,7 +93,7 @@ def test_bad_request end def test_cgi_env - start_cgi_server do |server, addr, port, log| + TestWEBrick.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/' @@ -137,7 +115,7 @@ def test_bad_uri assert_equal(1, log.length) assert_match(/ERROR bad URI/, log[0]) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.start_cgi_server({}, log_tester) {|server, addr, port, log| res = TCPSocket.open(addr, port) {|sock| sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}" sock.close_write @@ -155,7 +133,7 @@ def test_bad_header assert_equal(1, log.length) assert_match(/ERROR bad header/, log[0]) } - start_cgi_server(log_tester) {|server, addr, port, log| + TestWEBrick.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 diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb index 881fb54d..3b299d93 100644 --- a/test/webrick/test_filehandler.rb +++ b/test/webrick/test_filehandler.rb @@ -28,12 +28,12 @@ def get_res_body(res) end def make_range_request(range_spec) - msg = <<-END_OF_REQUEST + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.0 Range: #{range_spec} - END_OF_REQUEST - return StringIO.new(msg.gsub(/^ {6}/, "")) + HTTP + return StringIO.new(msg) end def make_range_response(file, range_spec) @@ -248,20 +248,15 @@ def test_unwise_in_path 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| + TestWEBrick.start_cgi_server({}, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) if windows? - root = config[:DocumentRoot].tr("/", "\\") + root = File.dirname(__FILE__).tr("/", "\\") fname = IO.popen(%W[dir /x #{root}\\webrick_long_filename.cgi], encoding: "binary", &:read) fname.sub!(/\A.*$^$.*$^$/m, '') if fname diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb index 2ff08d63..d1283d4e 100644 --- a/test/webrick/test_httprequest.rb +++ b/test/webrick/test_httprequest.rb @@ -10,21 +10,21 @@ def teardown end def test_simple_request - msg = <<-_end_of_message_ -GET / - _end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") + GET / + HTTP 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_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / foobar # HTTP/0.9 request don't have header nor entity body. - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version) @@ -36,12 +36,12 @@ def test_parse_09 end def test_parse_10 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET / HTTP/1.0 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version) @@ -53,12 +53,12 @@ def test_parse_10 end def test_parse_11 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal("GET", req.request_method) assert_equal("/path", req.unparsed_uri) assert_equal("", req.script_name) @@ -72,17 +72,173 @@ def test_parse_11 end def test_request_uri_too_large - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /#{"a"*2084} HTTP/1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){ - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) + } + end + + def test_invalid_content_length_header + ['', ' ', ' +1', ' -1', ' a'].each do |cl| + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1 + Content-Length:#{cl} + + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + end + + def test_bare_lf_request_line + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1 + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::EOFError){ + req.parse(StringIO.new(msg)) + } + end + + def test_bare_lf_header + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1\r + Content-Length: 0 + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + + def test_header_vt_ff_whitespace + msg = <<~HTTP + GET / HTTP/1.1\r + Foo: \x0b1\x0c\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("\x0b1\x0c", req["Foo"]) + + msg = <<~HTTP + GET / HTTP/1.1\r + Foo: \x0b1\x0c\r + \x0b2\x0c\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_equal("\x0b1\x0c \x0b2\x0c", req["Foo"]) + end + + def test_bare_cr_request_line + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1\r\r + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + + def test_bare_cr_header + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1\r + Content-Type: foo\rbar\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + + def test_invalid_request_lines + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1\r + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1\r + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + + msg = <<~HTTP.gsub("\n", "\r\n") + GET /\r HTTP/1.1\r + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1 \r + Content-Length: 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + + def test_duplicate_content_length_header + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.1 + Content-Length: 1 + Content-Length: 2 + + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.parse(StringIO.new(msg)) + } + end + + def test_content_length_and_transfer_encoding_headers_smuggling + msg = <<~HTTP.gsub("\n", "\r\n") + POST /user HTTP/1.1 + Content-Length: 28 + Transfer-Encoding: chunked + + 0 + + GET /admin HTTP/1.1 + + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ + req.body } end def test_parse_headers - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: test.ruby-lang.org:8080 Connection: close @@ -93,13 +249,13 @@ def test_parse_headers Accept-Language: en;q=0.5, *; q=0 Accept-Language: ja Content-Type: text/plain - Content-Length: 7 + Content-Length: 8 X-Empty-Header: foobar - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal( URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) @@ -110,88 +266,88 @@ def test_parse_headers 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(8, req.content_length) assert_equal("text/plain", req.content_type) - assert_equal("foobar\n", req.body) + assert_equal("foobar\r\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_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /foo/bar/../baz?q=a HTTP/1.0 - Content-Length: 9 + Content-Length: 10 User-Agent: FOO BAR BAZ hogehoge - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) 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("10", req['content-length']) assert_equal("FOO BAR BAZ", req['user-agent']) - assert_equal("hogehoge\n", req.body) + assert_equal("hogehoge\r\n", req.body) end def test_parse_headers3 - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: test.ruby-lang.org - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) 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_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: 192.168.1.1 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) 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_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7] - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) 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_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: 192.168.1.1:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) 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_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7]:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) @@ -200,13 +356,13 @@ def test_parse_headers3 def test_parse_get_params param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /path?#{param} HTTP/1.1 Host: test.ruby-lang.org:8080 - _end_of_message_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -217,16 +373,16 @@ def test_parse_get_params def test_parse_post_params param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") 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_ + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) @@ -238,13 +394,12 @@ def test_parse_post_params def test_chunked crlf = "\x0d\x0a" expect = File.binread(__FILE__).freeze - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 Host: test.ruby-lang.org:8080 Transfer-Encoding: chunked - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP File.open(__FILE__){|io| while chunk = io.read(100) msg << chunk.size.to_s(16) << crlf @@ -264,8 +419,63 @@ def test_chunked assert_equal(expect, dst.string) end + def test_bad_chunked + msg = <<~HTTP + POST /path HTTP/1.1\r + Transfer-Encoding: chunked\r + \r + 01x1\r + \r + 1 + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.body } + + # chunked req.body_reader + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + dst = StringIO.new + assert_raise(WEBrick::HTTPStatus::BadRequest) do + IO.copy_stream(req.body_reader, dst) + end + end + + def test_bad_chunked_extra_data + msg = <<~HTTP + POST /path HTTP/1.1\r + Transfer-Encoding: chunked\r + \r + 3\r + ABCthis-all-gets-ignored\r + 0\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.body } + + # chunked req.body_reader + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new(msg)) + dst = StringIO.new + assert_raise(WEBrick::HTTPStatus::BadRequest) do + IO.copy_stream(req.body_reader, dst) + end + end + + def test_null_byte_in_header + msg = <<~HTTP.gsub("\n", "\r\n") + POST /path HTTP/1.1\r + Evil: evil\x00\r + \r + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::BadRequest){ req.parse(StringIO.new(msg)) } + end + def test_forwarded - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 @@ -274,8 +484,7 @@ def test_forwarded X-Forwarded-Server: server.example.com Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -285,7 +494,7 @@ def test_forwarded assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 @@ -294,8 +503,7 @@ def test_forwarded X-Forwarded-Server: server.example.com Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -305,7 +513,7 @@ def test_forwarded assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -316,8 +524,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) @@ -327,7 +534,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -338,8 +545,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -349,7 +555,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -360,8 +566,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -371,7 +576,7 @@ def test_forwarded assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 @@ -382,8 +587,7 @@ def test_forwarded X-Requested-With: XMLHttpRequest Connection: Keep-Alive - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) @@ -395,12 +599,11 @@ def test_forwarded end def test_continue_sent - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 Expect: 100-continue - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert req['expect'] @@ -412,11 +615,10 @@ def test_continue_sent end def test_continue_not_sent - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") POST /path HTTP/1.1 - _end_of_message_ - msg.gsub!(/^ {6}/, "") + HTTP req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert !req['expect'] @@ -427,42 +629,42 @@ def test_continue_not_sent def test_bad_messages param = "foo=1;foo=2;foo=3;bar=x" - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") 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_ + HTTP assert_raise(WEBrick::HTTPStatus::LengthRequired){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) req.body } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") 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_ + HTTP assert_raise(WEBrick::HTTPStatus::BadRequest){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) req.body } - msg = <<-_end_of_message_ + msg = <<~HTTP.gsub("\n", "\r\n") 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_ + HTTP assert_raise(WEBrick::HTTPStatus::NotImplemented){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) - req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) + req.parse(StringIO.new(msg)) req.body } end @@ -473,4 +675,30 @@ def test_eof_raised_when_line_is_nil req.parse(StringIO.new("")) } end + + def test_eof_raised_with_missing_line_between_headers_and_body + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.0 + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::EOFError) { + req.parse(StringIO.new(msg)) + } + + msg = <<~HTTP.gsub("\n", "\r\n") + GET / HTTP/1.0 + Foo: 1 + HTTP + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + assert_raise(WEBrick::HTTPStatus::EOFError) { + req.parse(StringIO.new(msg)) + } + end + + def test_cookie_join + req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) + req.parse(StringIO.new("GET / HTTP/1.1\r\ncookie: a=1\r\ncookie: b=2\r\n\r\n")) + assert_equal 2, req.cookies.length + assert_equal 'a=1; b=2', req['cookie'] + end end diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb index 83734533..6c4a1794 100644 --- a/test/webrick/test_httpresponse.rb +++ b/test/webrick/test_httpresponse.rb @@ -97,14 +97,14 @@ def test_prevent_response_splitting_cookie_headers_lf def test_set_redirect_response_splitting url = "malicious\r\nCookie: cracked_indicator_for_test" - assert_raises(URI::InvalidURIError) do + assert_raise(URI::InvalidURIError) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end end def test_set_redirect_html_injection url = 'http://example.com////?a' - assert_raises(WEBrick::HTTPStatus::MultipleChoices) do + assert_raise(WEBrick::HTTPStatus::MultipleChoices) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end res.status = 300 diff --git a/test/webrick/utils.rb b/test/webrick/utils.rb index a8568d0a..c8e84c37 100644 --- a/test/webrick/utils.rb +++ b/test/webrick/utils.rb @@ -81,4 +81,24 @@ def start_httpserver(config={}, log_tester=DefaultLogTester, &block) def start_httpproxy(config={}, log_tester=DefaultLogTester, &block) start_server(WEBrick::HTTPProxyServer, config, log_tester, &block) end + + def start_cgi_server(config={}, 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 + }, + }.merge(config) + if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ + config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. + end + start_server(WEBrick::HTTPServer, config, log_tester, &block) + end end