Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
14 changes: 10 additions & 4 deletions lib/net/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/, '')
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions net-http.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 6 additions & 5 deletions test/net/http/test_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 65 additions & 1 deletion test/net/http/test_httpresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(<<EOS)
HTTP/1.1 200 OK
Expand Down Expand Up @@ -775,4 +810,33 @@ def dummy_io(str)

Net::BufferedIO.new(StringIO.new(str))
end

# Yields the given prefix, then streams 'a' without EOF. Stops with an
# error well past the limit under test so that a regression fails the
# test instead of looping until the machine runs out of memory.
class EndlessDataIO
SAFETY_LIMIT = Net::HTTPResponse::MAX_RESPONSE_HEADER_LENGTH * 2

def initialize(prefix)
@buf = prefix.b
@served = 0
end

def read_nonblock(size, buf = nil, exception: false)
@served += size
raise "read #{@served} bytes without hitting the line limit" if
@served > 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