1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Set CONTENT_LENGTH for chunked requests

Chunked requests don't contain a Content-Length header, but Puma buffers
the entire request body upfront, which means it can determine the length
before dispatching to the application.

The Rack spec doesn't mandate the presence of the CONTENT_LENGTH header,
but it does refer to it as a "CGI key" and draws a distinction between
it and the HTTP Content-Length header:

https://github.com/rack/rack/blob/v2.2.2/SPEC.rdoc

> The environment must not contain the keys HTTP_CONTENT_TYPE or
> HTTP_CONTENT_LENGTH (use the versions without HTTP_). The CGI keys
> (named without a period) must have String values.

RFC 3875, which defines the CGI protocol including CONTENT_LENGTH, says:

https://tools.ietf.org/html/rfc3875#section-4.1.2

> The server MUST set this meta-variable if and only if the request is
> accompanied by a message-body entity. The CONTENT_LENGTH value must
> reflect the length of the message-body after the server has removed
> any transfer-codings or content-codings.

"Removing a transfer-coding" is precisely what Puma is doing when it
parses a chunked request.

RFC 7230, the most recent specification of HTTP 1.1, includes a pseudo-
code algorithm for decoding chunked requests that roughly matches the
behaviour implemented here:

https://tools.ietf.org/html/rfc7230#section-4.1.3
This commit is contained in:
Eugene Kenny 2020-05-29 01:35:10 +01:00
parent 22002acc9d
commit bdcfb60117
3 changed files with 60 additions and 7 deletions

View file

@ -46,6 +46,7 @@
* Improvements to `out_of_band` hook (#2234)
* Prefer the rackup file specified by the CLI (#2225)
* Fix for spawning subprocesses with fork_worker option (#2267)
* Set `CONTENT_LENGTH` for chunked requests (#2287)
* Refactor
* Remove unused loader argument from Plugin initializer (#2095)

View file

@ -420,7 +420,10 @@ module Puma
raise EOFError
end
return true if decode_chunk(chunk)
if decode_chunk(chunk)
@env[CONTENT_LENGTH] = @chunked_content_length
return true
end
end
end
@ -432,20 +435,28 @@ module Puma
@body = Tempfile.new(Const::PUMA_TMP_BASE)
@body.binmode
@tempfile = @body
@chunked_content_length = 0
return decode_chunk(body)
if decode_chunk(body)
@env[CONTENT_LENGTH] = @chunked_content_length
return true
end
end
def write_chunk(str)
@chunked_content_length += @body.write(str)
end
def decode_chunk(chunk)
if @partial_part_left > 0
if @partial_part_left <= chunk.size
if @partial_part_left > 2
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
end
chunk = chunk[@partial_part_left..-1]
@partial_part_left = 0
else
@body << chunk if @partial_part_left > 2 # don't include the last \r\n
write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
@partial_part_left -= chunk.size
return false
end
@ -492,12 +503,12 @@ module Puma
case
when got == len
@body << part[0..-3] # to skip the ending \r\n
write_chunk(part[0..-3]) # to skip the ending \r\n
when got <= len - 2
@body << part
write_chunk(part)
@partial_part_left = len - part.size
when got == len - 1 # edge where we get just \r but not \n
@body << part[0..-2]
write_chunk(part[0..-2])
@partial_part_left = len - part.size
end
else

View file

@ -498,8 +498,10 @@ EOF
def test_chunked_request
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -507,12 +509,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_before_value
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -525,12 +530,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_between_chunks
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -543,12 +551,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_mid_count
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -561,12 +572,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_before_count_newline
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -579,12 +593,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_mid_value
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -597,12 +614,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_request_pause_between_cr_lf_after_size_of_second_chunk
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -624,12 +644,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal (part1 + 'b'), body
assert_equal 4201, content_length
end
def test_chunked_request_pause_between_closing_cr_lf
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -643,12 +666,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal 'hello', body
assert_equal 5, content_length
end
def test_chunked_request_pause_before_closing_cr_lf
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -662,12 +688,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal 'hello', body
assert_equal 5, content_length
end
def test_chunked_request_header_case
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -675,12 +704,15 @@ EOF
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal "hello", body
assert_equal 5, content_length
end
def test_chunked_keep_alive
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -690,14 +722,17 @@ EOF
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], h
assert_equal "hello", body
assert_equal 5, content_length
sock.close
end
def test_chunked_keep_alive_two_back_to_back
body = nil
content_length = nil
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
[200, {}, [""]]
}
@ -715,6 +750,7 @@ EOF
h = header(sock)
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], h
assert_equal "hello", body
assert_equal 5, content_length
assert_equal true, last_crlf_written
last_crlf_writer.join
@ -726,16 +762,19 @@ EOF
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], h
assert_equal "goodbye", body
assert_equal 7, content_length
sock.close
end
def test_chunked_keep_alive_two_back_to_back_with_set_remote_address
body = nil
content_length = nil
remote_addr =nil
@server = Puma::Server.new @app, @events, { remote_address: :header, remote_address_header: 'HTTP_X_FORWARDED_FOR'}
server_run app: ->(env) {
body = env['rack.input'].read
content_length = env['CONTENT_LENGTH']
remote_addr = env['REMOTE_ADDR']
[200, {}, [""]]
}
@ -745,6 +784,7 @@ EOF
h = header sock
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], h
assert_equal "hello", body
assert_equal 5, content_length
assert_equal "127.0.0.1", remote_addr
sock << "GET / HTTP/1.1\r\nX-Forwarded-For: 127.0.0.2\r\nConnection: Keep-Alive\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ngood\r\n3\r\nbye\r\n0\r\n\r\n"
@ -754,6 +794,7 @@ EOF
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], h
assert_equal "goodbye", body
assert_equal 7, content_length
assert_equal "127.0.0.2", remote_addr
sock.close