diff --git a/lib/protocol/multipart/form_data.rb b/lib/protocol/multipart/form_data.rb index 29488ec..56cd459 100644 --- a/lib/protocol/multipart/form_data.rb +++ b/lib/protocol/multipart/form_data.rb @@ -16,27 +16,27 @@ module Multipart class FormData < Mixed include Escape - # The default maximum size of a buffered form field. - MAXIMUM_FIELD_SIZE = 2 * 1024 * 1024 + # The buffered form field size limit. + FIELD_SIZE_LIMIT = 2 * 1024 * 1024 - # The default maximum size of a streamed file upload. - MAXIMUM_UPLOAD_SIZE = 128 * 1024 * 1024 + # The streamed file upload size limit. + UPLOAD_SIZE_LIMIT = 128 * 1024 * 1024 - # The default maximum combined size of all form fields and file uploads. - MAXIMUM_TOTAL_SIZE = 256 * 1024 * 1024 + # The combined size limit for all form fields and file uploads. + TOTAL_SIZE_LIMIT = 256 * 1024 * 1024 # A file upload yielded while parsing form data. class Upload # Initialize a streamed file upload. # @parameter part [Parser::Part] The underlying multipart part. # @parameter filename [String] The submitted filename. - # @parameter maximum_size [Integer | Nil] The maximum upload size. - # @parameter total_limit [ByteLimit] The shared form-data size limit. - def initialize(part, filename, maximum_size, total_limit) + # @parameter size_limit [Integer | Nil] The upload size limit. + # @parameter total_size_limit [ByteLimit] The shared form-data size limit. + def initialize(part, filename, size_limit, total_size_limit) @part = part @filename = filename - @limit = ByteLimit.new(maximum_size, name: :upload_size) - @total_limit = total_limit + @size_limit = ByteLimit.new(size_limit, name: :upload_size) + @total_size_limit = total_size_limit end # The submitted filename. @@ -49,7 +49,7 @@ def headers # The number of upload bytes consumed so far. def size - @limit.size + @size_limit.size end # Whether the complete upload has been consumed. @@ -63,8 +63,8 @@ def each(chunk_size = 8192) return to_enum(:each, chunk_size) unless block_given? @part.each(chunk_size) do |chunk| - @limit.consume(chunk.bytesize) - @total_limit.consume(chunk.bytesize) + @size_limit.consume(chunk.bytesize) + @total_size_limit.consume(chunk.bytesize) yield chunk end diff --git a/lib/protocol/multipart/form_data/parser.rb b/lib/protocol/multipart/form_data/parser.rb index fbc1aaa..997fcff 100644 --- a/lib/protocol/multipart/form_data/parser.rb +++ b/lib/protocol/multipart/form_data/parser.rb @@ -15,20 +15,20 @@ class Parser CONTENT_TYPE = "multipart/form-data" # Initialize the form data parser. - # @parameter maximum_field_size [Integer | Nil] The maximum size of each buffered field. - # @parameter maximum_upload_size [Integer | Nil] The maximum size of each file upload. - # @parameter maximum_total_size [Integer | Nil] The maximum combined size of all fields and uploads. - # @parameter maximum_depth [Integer | Nil] The maximum depth of a bracketed form name. + # @parameter field_size_limit [Integer | Nil] The buffered field size limit. + # @parameter upload_size_limit [Integer | Nil] The file upload size limit. + # @parameter total_size_limit [Integer | Nil] The combined size limit for all fields and uploads. + # @parameter depth_limit [Integer | Nil] The bracketed form name depth limit. # @parameter options [Hash] Limits passed to the underlying multipart parser. - def initialize(maximum_field_size: MAXIMUM_FIELD_SIZE, maximum_upload_size: MAXIMUM_UPLOAD_SIZE, maximum_total_size: MAXIMUM_TOTAL_SIZE, maximum_depth: Protocol::URL::FormData::Nested::MAXIMUM_DEPTH, **options) - if maximum_depth and maximum_depth < 0 + def initialize(field_size_limit: FIELD_SIZE_LIMIT, upload_size_limit: UPLOAD_SIZE_LIMIT, total_size_limit: TOTAL_SIZE_LIMIT, depth_limit: Protocol::URL::FormData::Nested::DEPTH_LIMIT, **options) + if depth_limit and depth_limit < 0 raise ArgumentError, "Form data limits must be non-negative!" end - @maximum_field_size = maximum_field_size - @maximum_upload_size = maximum_upload_size - @maximum_total_size = maximum_total_size - @maximum_depth = maximum_depth + @field_size_limit = field_size_limit + @upload_size_limit = upload_size_limit + @total_size_limit = total_size_limit + @depth_limit = depth_limit @options = options end @@ -66,7 +66,7 @@ def parse(readable, result = make_result, boundary:) def each(readable, boundary:) return to_enum(__method__, readable, boundary:) unless block_given? - total_limit = ByteLimit.new(@maximum_total_size, name: :total_size) + total_size_limit = ByteLimit.new(@total_size_limit, name: :total_size) parser = Multipart::Parser.new(readable, boundary, **@options) parser.each do |part| @@ -77,16 +77,16 @@ def each(readable, boundary:) end if filename = disposition["filename"] - upload = Upload.new(part, filename, @maximum_upload_size, total_limit) + upload = Upload.new(part, filename, @upload_size_limit, total_size_limit) yield name, upload upload.discard else - field_limit = ByteLimit.new(@maximum_field_size, name: :field_size) + field_size_limit = ByteLimit.new(@field_size_limit, name: :field_size) value = String.new.b part.each do |chunk| - field_limit.consume(chunk.bytesize) - total_limit.consume(chunk.bytesize) + field_size_limit.consume(chunk.bytesize) + total_size_limit.consume(chunk.bytesize) value << chunk end @@ -98,7 +98,7 @@ def each(readable, boundary:) private def make_result - return Protocol::URL::FormData::Nested.new(maximum_depth: @maximum_depth) + return Protocol::URL::FormData::Nested.new(depth_limit: @depth_limit) end end end diff --git a/lib/protocol/multipart/parser.rb b/lib/protocol/multipart/parser.rb index bb0f046..2587438 100644 --- a/lib/protocol/multipart/parser.rb +++ b/lib/protocol/multipart/parser.rb @@ -14,17 +14,17 @@ class Parser HEADER_PATTERN = /\A([!-9;-~]+):[ \t]*([^\x00-\x08\x0a-\x1f\x7f]*)\z/.freeze private_constant :HEADER_PATTERN - # The default maximum number of preamble bytes before the first boundary. - MAXIMUM_PREAMBLE_SIZE = 64 * 1024 + # The preamble size limit. + PREAMBLE_SIZE_LIMIT = 64 * 1024 - # The default maximum number of header bytes in each part. - MAXIMUM_HEADER_SIZE = 64 * 1024 + # The header size limit for each part. + HEADER_SIZE_LIMIT = 64 * 1024 - # The default maximum number of headers in each part. - MAXIMUM_HEADER_COUNT = 64 + # The header count limit for each part. + HEADER_COUNT_LIMIT = 64 - # The default maximum number of parts. - MAXIMUM_PART_COUNT = 128 + # The part count limit. + PART_COUNT_LIMIT = 128 # Represents a single part within a multipart message. class Part @@ -163,12 +163,12 @@ def closing_boundary? # # @parameter readable [IO, IO::Stream] The readable stream containing multipart data. # @parameter boundary [String] The boundary string that separates the parts. - # @parameter maximum_preamble_size [Integer | Nil] The maximum preamble size, or nil for no limit. - # @parameter maximum_header_size [Integer | Nil] The maximum header size per part, or nil for no limit. - # @parameter maximum_header_count [Integer | Nil] The maximum header count per part, or nil for no limit. - # @parameter maximum_part_count [Integer | Nil] The maximum part count, or nil for no limit. - def initialize(readable, boundary, maximum_preamble_size: MAXIMUM_PREAMBLE_SIZE, maximum_header_size: MAXIMUM_HEADER_SIZE, maximum_header_count: MAXIMUM_HEADER_COUNT, maximum_part_count: MAXIMUM_PART_COUNT) - limits = [maximum_preamble_size, maximum_header_size, maximum_header_count, maximum_part_count] + # @parameter preamble_size_limit [Integer | Nil] The preamble size limit, or nil for no limit. + # @parameter header_size_limit [Integer | Nil] The header size limit per part, or nil for no limit. + # @parameter header_count_limit [Integer | Nil] The header count limit per part, or nil for no limit. + # @parameter part_count_limit [Integer | Nil] The part count limit, or nil for no limit. + def initialize(readable, boundary, preamble_size_limit: PREAMBLE_SIZE_LIMIT, header_size_limit: HEADER_SIZE_LIMIT, header_count_limit: HEADER_COUNT_LIMIT, part_count_limit: PART_COUNT_LIMIT) + limits = [preamble_size_limit, header_size_limit, header_count_limit, part_count_limit] if limits.any?{|limit| limit and limit < 0} raise ArgumentError, "Multipart limits must be non-negative!" @@ -176,10 +176,10 @@ def initialize(readable, boundary, maximum_preamble_size: MAXIMUM_PREAMBLE_SIZE, @readable = IO::Stream(readable) @boundary = boundary - @maximum_preamble_size = maximum_preamble_size - @maximum_header_size = maximum_header_size - @maximum_header_count = maximum_header_count - @maximum_part_count = maximum_part_count + @preamble_size_limit = preamble_size_limit + @header_size_limit = header_size_limit + @header_count_limit = header_count_limit + @part_count_limit = part_count_limit @boundary_marker = "--#{@boundary}\r\n".freeze end @@ -195,12 +195,12 @@ def each # Read lines until we find the first boundary: while true - if line = read_line(preamble_size, @maximum_preamble_size, allowance: @boundary_marker.bytesize, chomp: false) + if line = read_line(preamble_size, @preamble_size_limit, allowance: @boundary_marker.bytesize, chomp: false) if line == @boundary_marker break else preamble_size += line.bytesize - check_limit(:preamble_size, preamble_size, @maximum_preamble_size) + check_limit(:preamble_size, preamble_size, @preamble_size_limit) end else # End of stream reached without finding boundary: @@ -212,7 +212,7 @@ def each while true part_count += 1 - check_limit(:part_count, part_count, @maximum_part_count) + check_limit(:part_count, part_count, @part_count_limit) part = read_part break unless part @@ -234,18 +234,18 @@ def each private - def read_line(size, maximum, allowance: 0, chomp:) - if maximum - limit = maximum - size + allowance + 1 + def read_line(size, limit, allowance: 0, chomp:) + if limit + limit = limit - size + allowance + 1 return @readable.gets("\r\n", limit, chomp: chomp) else return @readable.gets("\r\n", chomp: chomp) end end - def check_limit(name, value, maximum) - if maximum and value > maximum - raise RangeError, "Multipart #{name} exceeded limit of #{maximum}!" + def check_limit(name, value, limit) + if limit and value > limit + raise RangeError, "Multipart #{name} exceeded limit of #{limit}!" end end @@ -255,18 +255,18 @@ def read_part header_count = 0 # Read headers until empty line - while line = read_line(header_size, @maximum_header_size, allowance: 2, chomp: true) + while line = read_line(header_size, @header_size_limit, allowance: 2, chomp: true) if line.empty? break # End of headers end header_size += line.bytesize + 2 - check_limit(:header_size, header_size, @maximum_header_size) + check_limit(:header_size, header_size, @header_size_limit) if match = line.match(HEADER_PATTERN) # Parse header line (name: value) header_count += 1 - check_limit(:header_count, header_count, @maximum_header_count) + check_limit(:header_count, header_count, @header_count_limit) fields << [match[1], match[2].strip] else diff --git a/releases.md b/releases.md index fbe1a7b..fa7db27 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Use consistent limit naming for multipart parser constraints. + ## v0.3.0 - Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments. diff --git a/test/protocol/multipart/form_data.rb b/test/protocol/multipart/form_data.rb index aa92dd4..342f25d 100644 --- a/test/protocol/multipart/form_data.rb +++ b/test/protocol/multipart/form_data.rb @@ -171,7 +171,7 @@ def to_h form_data.add_field("field", "content") expect do - subject::Parser.new(maximum_field_size: 3).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a + subject::Parser.new(field_size_limit: 3).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a end.to raise_exception(RangeError, message: be =~ /field_size exceeded/) end @@ -182,7 +182,7 @@ def to_h ) expect do - subject::Parser.new(maximum_upload_size: 3).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a + subject::Parser.new(upload_size_limit: 3).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a end.to raise_exception(RangeError, message: be =~ /upload_size exceeded/) end @@ -191,7 +191,7 @@ def to_h form_data.add_field("second", "two") expect do - subject::Parser.new(maximum_total_size: 5).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a + subject::Parser.new(total_size_limit: 5).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a end.to raise_exception(RangeError, message: be =~ /total_size exceeded/) end @@ -199,9 +199,9 @@ def to_h form_data.add_field("field", "content") parser = subject::Parser.new( - maximum_field_size: nil, - maximum_upload_size: nil, - maximum_total_size: nil, + field_size_limit: nil, + upload_size_limit: nil, + total_size_limit: nil, ) values = parser.each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a @@ -210,13 +210,13 @@ def to_h it "rejects negative content limits" do expect do - subject::Parser.new(maximum_total_size: -1).each(StringIO.new, boundary: "boundary").to_a + subject::Parser.new(total_size_limit: -1).each(StringIO.new, boundary: "boundary").to_a end.to raise_exception(ArgumentError, message: be =~ /must be non-negative/) end it "rejects a negative nesting limit" do expect do - subject::Parser.new(maximum_depth: -1) + subject::Parser.new(depth_limit: -1) end.to raise_exception(ArgumentError, message: be =~ /must be non-negative/) end @@ -224,7 +224,7 @@ def to_h form_data.add_field("a[b][c]", "value") expect do - subject::Parser.new(maximum_depth: 2).parse(StringIO.new(serialize(form_data)), boundary: form_data.boundary) + subject::Parser.new(depth_limit: 2).parse(StringIO.new(serialize(form_data)), boundary: form_data.boundary) end.to raise_exception(RangeError, message: be =~ /depth exceeded/) end diff --git a/test/protocol/multipart/parser.rb b/test/protocol/multipart/parser.rb index 05a96cb..164903d 100644 --- a/test/protocol/multipart/parser.rb +++ b/test/protocol/multipart/parser.rb @@ -54,69 +54,69 @@ it "limits the preamble size" do data = "preamble\r\n--#{boundary}\r\n\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_preamble_size: 4) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, preamble_size_limit: 4) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /preamble_size exceeded/) end it "limits an unterminated preamble" do - parser = Protocol::Multipart::Parser.new(StringIO.new("x" * 1024), boundary, maximum_preamble_size: 16) + parser = Protocol::Multipart::Parser.new(StringIO.new("x" * 1024), boundary, preamble_size_limit: 16) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /preamble_size exceeded/) end it "allows a boundary after the maximum preamble size" do data = "x\r\n--#{boundary}\r\n\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_preamble_size: 3) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, preamble_size_limit: 3) expect(parser.each.to_a).to be(:empty?) end it "limits each part's header size" do data = "--#{boundary}\r\nContent-Type: text/plain\r\n\r\nvalue\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_header_size: 16) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_size_limit: 16) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_size exceeded/) end it "limits an unterminated header" do data = "--#{boundary}\r\nX-Test: #{'x' * 1024}" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_header_size: 16) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_size_limit: 16) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_size exceeded/) end it "allows the header terminator after the maximum header size" do data = "--#{boundary}\r\nX: y\r\n\r\nvalue\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_header_size: 6) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_size_limit: 6) expect(parser.each.to_a.size).to be == 1 end it "limits each part's header count" do data = "--#{boundary}\r\nContent-Type: text/plain\r\nX-Test: true\r\n\r\nvalue\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_header_count: 1) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_count_limit: 1) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_count exceeded/) end it "limits the part count" do data = "--#{boundary}\r\n\r\none\r\n--#{boundary}\r\n\r\ntwo\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_part_count: 1) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, part_count_limit: 1) expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /part_count exceeded/) end it "allows limits to be disabled" do data = "preamble\r\n--#{boundary}\r\nContent-Type: text/plain\r\n\r\nvalue\r\n--#{boundary}--\r\n" - parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, maximum_preamble_size: nil, maximum_header_size: nil, maximum_header_count: nil, maximum_part_count: nil) + parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, preamble_size_limit: nil, header_size_limit: nil, header_count_limit: nil, part_count_limit: nil) expect(parser.each.to_a.size).to be == 1 end it "rejects negative limits" do expect do - Protocol::Multipart::Parser.new(StringIO.new, boundary, maximum_part_count: -1) + Protocol::Multipart::Parser.new(StringIO.new, boundary, part_count_limit: -1) end.to raise_exception(ArgumentError, message: be =~ /must be non-negative/) end