diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56ded6f..faec14e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,8 @@ jobs: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} + env: + BUNDLE_WITHOUT: sig steps: - uses: actions/checkout@v7 - name: Set up Ruby @@ -26,11 +28,9 @@ jobs: ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: bundle install - env: - BUNDLE_WITHOUT: sig - name: Run test - run: rake test + run: bundle exec rake test - name: Build - run: rake build + run: bundle exec rake build env: LANG: C diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb index 0b5b326..4d8b938 100644 --- a/lib/net/http/response.rb +++ b/lib/net/http/response.rb @@ -155,11 +155,17 @@ def read_new(sock) #:nodoc: internal use only res end + def read_line(sock, limit, ignore_eof = false) #:nodoc: internal use only + sock.readuntil("\n", ignore_eof, limit: limit) + rescue Net::ReadLimitExceeded + raise Net::HTTPBadResponse, 'response line too long' + end + private # :stopdoc: def read_status_line(sock) - str = sock.readline + str = read_line(sock, MAX_RESPONSE_HEADER_LENGTH).chop m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" m.captures @@ -175,7 +181,7 @@ def each_response_header(sock) key = value = nil remaining = MAX_RESPONSE_HEADER_LENGTH while true - line = sock.readuntil("\n", true) + line = read_line(sock, MAX_RESPONSE_HEADER_LENGTH, true) remaining -= line.bytesize raise Net::HTTPBadResponse, 'response header too large' if remaining < 0 line = line.sub(/\s+\z/, '') @@ -630,7 +636,7 @@ def read_body_0(dest) def read_chunked(dest, chunk_data_io) # :nodoc: total = 0 while true - line = @socket.readline + line = self.class.read_line(@socket, MAX_RESPONSE_HEADER_LENGTH).chop hexlen = line.slice(/[0-9a-fA-F]+/) or raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" len = hexlen.hex @@ -642,7 +648,7 @@ def read_chunked(dest, chunk_data_io) # :nodoc: @socket.read 2 # \r\n end end - until @socket.readline.empty? + until self.class.read_line(@socket, MAX_RESPONSE_HEADER_LENGTH).chop.empty? # none end end diff --git a/net-http.gemspec b/net-http.gemspec index d59d5c3..318f9dc 100644 --- a/net-http.gemspec +++ b/net-http.gemspec @@ -35,5 +35,6 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.require_paths = ["lib"] + spec.add_dependency "net-protocol", ">= 0.3.0" spec.add_dependency "uri", ">= 0.11.1" end diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index e5028d4..097fda6 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -1262,17 +1262,18 @@ def closed? end def write(_) end - def readline + def readuntil(terminator, ignore_eof = false, limit: nil) + raise "unexpected terminator #{terminator.dump}" unless terminator == "\n" + # A header read ends the headers at once. Every other read is the + # status line of a fresh attempt, which is what count measures. + return "" if ignore_eof @count += 1 if @success_after && @success_after <= @count - "HTTP/1.1 200 OK" + "HTTP/1.1 200 OK\n" else raise Errno::ECONNRESET end end - def readuntil(*_) - "" - end def read_all(_) end end diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index 7e7ae8a..7f9ea94 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -25,9 +25,10 @@ def test_response_header_too_large count.times { |i| headers << "X-Pad-#{i}: #{big_value}\n" } headers << "\nhello\n" io = dummy_io(headers) - assert_raise(Net::HTTPBadResponse) do + e = assert_raise(Net::HTTPBadResponse) do Net::HTTPResponse.read_new(io) end + assert_equal 'response header too large', e.message end def test_response_header_within_limit @@ -42,6 +43,40 @@ def test_response_header_within_limit end end + def test_status_line_too_long + io = endless_io("HTTP/1.1 200 ") + assert_raise(Net::HTTPBadResponse) do + Net::HTTPResponse.read_new(io) + end + end + + def test_response_header_line_too_long + io = endless_io("HTTP/1.1 200 OK\nX-Foo: ") + assert_raise(Net::HTTPBadResponse) do + Net::HTTPResponse.read_new(io) + end + end + + def test_chunk_size_line_too_long + io = endless_io("HTTP/1.1 200 OK\nTransfer-Encoding: chunked\n\n") + res = Net::HTTPResponse.read_new(io) + assert_raise(Net::HTTPBadResponse) do + res.reading_body io, true do + res.read_body + end + end + end + + def test_chunk_trailer_line_too_long + io = endless_io("HTTP/1.1 200 OK\nTransfer-Encoding: chunked\n\n0\n") + res = Net::HTTPResponse.read_new(io) + assert_raise(Net::HTTPBadResponse) do + res.reading_body io, true do + res.read_body + end + end + end + def test_multiline_header io = dummy_io(< SAFETY_LIMIT + @buf << 'a' * (size - @buf.bytesize) if @buf.bytesize < size + s = @buf.slice!(0, size) + buf ? buf.replace(s) : s + end + + def closed? + false + end + end + + def endless_io(prefix) + Net::BufferedIO.new(EndlessDataIO.new(prefix.gsub(/\n/, "\r\n"))) + end end