protocol-multipart 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/multipart/byte_limit.rb +3 -1
- data/lib/protocol/multipart/error.rb +12 -0
- data/lib/protocol/multipart/form_data/parser.rb +16 -16
- data/lib/protocol/multipart/form_data.rb +14 -14
- data/lib/protocol/multipart/parser.rb +31 -30
- data/lib/protocol/multipart/version.rb +1 -1
- data/lib/protocol/multipart.rb +1 -0
- data/readme.md +8 -0
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a837f15946884597a70197efe8b11ecb4826bdb7c504eee9a60fe1b1d6e0666
|
|
4
|
+
data.tar.gz: 0c0c373cddf15634d278db2dc3ab8fea3db9055712d719356c4507ea68c2022c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '039281289f4c94fca34ba72ad62d58be53fba69571f355e6f5ceb1c01580900d8bbc7470ca167866c336f357d6568dfc8b2c63b766c1aed1dd0fd69b2b2d039d'
|
|
7
|
+
data.tar.gz: 821046db0d9fdbb001dc4dbcace537bdbbf5d04b80d0916643787f04741c53cbf5ba8bbb78b0a022cc19fbf43c9b81eb5639aaced247c06c4d2a864247d1efef
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "error"
|
|
7
|
+
|
|
6
8
|
module Protocol
|
|
7
9
|
module Multipart
|
|
8
10
|
# Tracks consumed bytes against an optional maximum.
|
|
@@ -30,7 +32,7 @@ module Protocol
|
|
|
30
32
|
@size += size
|
|
31
33
|
|
|
32
34
|
if @maximum and @size > @maximum
|
|
33
|
-
raise
|
|
35
|
+
raise LimitError, "Multipart #{@name} exceeded limit of #{@maximum}!"
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
return @size
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Protocol
|
|
7
|
+
module Multipart
|
|
8
|
+
# Raised when multipart processing exceeds a configured limit.
|
|
9
|
+
class LimitError < StandardError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -15,20 +15,20 @@ module Protocol
|
|
|
15
15
|
CONTENT_TYPE = "multipart/form-data"
|
|
16
16
|
|
|
17
17
|
# Initialize the form data parser.
|
|
18
|
-
# @parameter
|
|
19
|
-
# @parameter
|
|
20
|
-
# @parameter
|
|
21
|
-
# @parameter
|
|
18
|
+
# @parameter field_size_limit [Integer | Nil] The buffered field size limit.
|
|
19
|
+
# @parameter upload_size_limit [Integer | Nil] The file upload size limit.
|
|
20
|
+
# @parameter total_size_limit [Integer | Nil] The combined size limit for all fields and uploads.
|
|
21
|
+
# @parameter depth_limit [Integer | Nil] The bracketed form name depth limit.
|
|
22
22
|
# @parameter options [Hash] Limits passed to the underlying multipart parser.
|
|
23
|
-
def initialize(
|
|
24
|
-
if
|
|
23
|
+
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)
|
|
24
|
+
if depth_limit and depth_limit < 0
|
|
25
25
|
raise ArgumentError, "Form data limits must be non-negative!"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
@
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
@
|
|
28
|
+
@field_size_limit = field_size_limit
|
|
29
|
+
@upload_size_limit = upload_size_limit
|
|
30
|
+
@total_size_limit = total_size_limit
|
|
31
|
+
@depth_limit = depth_limit
|
|
32
32
|
@options = options
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -66,7 +66,7 @@ module Protocol
|
|
|
66
66
|
def each(readable, boundary:)
|
|
67
67
|
return to_enum(__method__, readable, boundary:) unless block_given?
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
total_size_limit = ByteLimit.new(@total_size_limit, name: :total_size)
|
|
70
70
|
parser = Multipart::Parser.new(readable, boundary, **@options)
|
|
71
71
|
|
|
72
72
|
parser.each do |part|
|
|
@@ -77,16 +77,16 @@ module Protocol
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
if filename = disposition["filename"]
|
|
80
|
-
upload = Upload.new(part, filename, @
|
|
80
|
+
upload = Upload.new(part, filename, @upload_size_limit, total_size_limit)
|
|
81
81
|
yield name, upload
|
|
82
82
|
upload.discard
|
|
83
83
|
else
|
|
84
|
-
|
|
84
|
+
field_size_limit = ByteLimit.new(@field_size_limit, name: :field_size)
|
|
85
85
|
value = String.new.b
|
|
86
86
|
|
|
87
87
|
part.each do |chunk|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
field_size_limit.consume(chunk.bytesize)
|
|
89
|
+
total_size_limit.consume(chunk.bytesize)
|
|
90
90
|
value << chunk
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -98,7 +98,7 @@ module Protocol
|
|
|
98
98
|
private
|
|
99
99
|
|
|
100
100
|
def make_result
|
|
101
|
-
return Protocol::URL::FormData::Nested.new(
|
|
101
|
+
return Protocol::URL::FormData::Nested.new(depth_limit: @depth_limit)
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -16,27 +16,27 @@ module Protocol
|
|
|
16
16
|
class FormData < Mixed
|
|
17
17
|
include Escape
|
|
18
18
|
|
|
19
|
-
# The
|
|
20
|
-
|
|
19
|
+
# The buffered form field size limit.
|
|
20
|
+
FIELD_SIZE_LIMIT = 2 * 1024 * 1024
|
|
21
21
|
|
|
22
|
-
# The
|
|
23
|
-
|
|
22
|
+
# The streamed file upload size limit.
|
|
23
|
+
UPLOAD_SIZE_LIMIT = 128 * 1024 * 1024
|
|
24
24
|
|
|
25
|
-
# The
|
|
26
|
-
|
|
25
|
+
# The combined size limit for all form fields and file uploads.
|
|
26
|
+
TOTAL_SIZE_LIMIT = 256 * 1024 * 1024
|
|
27
27
|
|
|
28
28
|
# A file upload yielded while parsing form data.
|
|
29
29
|
class Upload
|
|
30
30
|
# Initialize a streamed file upload.
|
|
31
31
|
# @parameter part [Parser::Part] The underlying multipart part.
|
|
32
32
|
# @parameter filename [String] The submitted filename.
|
|
33
|
-
# @parameter
|
|
34
|
-
# @parameter
|
|
35
|
-
def initialize(part, filename,
|
|
33
|
+
# @parameter size_limit [Integer | Nil] The upload size limit.
|
|
34
|
+
# @parameter total_size_limit [ByteLimit] The shared form-data size limit.
|
|
35
|
+
def initialize(part, filename, size_limit, total_size_limit)
|
|
36
36
|
@part = part
|
|
37
37
|
@filename = filename
|
|
38
|
-
@
|
|
39
|
-
@
|
|
38
|
+
@size_limit = ByteLimit.new(size_limit, name: :upload_size)
|
|
39
|
+
@total_size_limit = total_size_limit
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# The submitted filename.
|
|
@@ -49,7 +49,7 @@ module Protocol
|
|
|
49
49
|
|
|
50
50
|
# The number of upload bytes consumed so far.
|
|
51
51
|
def size
|
|
52
|
-
@
|
|
52
|
+
@size_limit.size
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Whether the complete upload has been consumed.
|
|
@@ -63,8 +63,8 @@ module Protocol
|
|
|
63
63
|
return to_enum(:each, chunk_size) unless block_given?
|
|
64
64
|
|
|
65
65
|
@part.each(chunk_size) do |chunk|
|
|
66
|
-
@
|
|
67
|
-
@
|
|
66
|
+
@size_limit.consume(chunk.bytesize)
|
|
67
|
+
@total_size_limit.consume(chunk.bytesize)
|
|
68
68
|
yield chunk
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "io/stream"
|
|
7
|
+
require_relative "error"
|
|
7
8
|
require_relative "headers"
|
|
8
9
|
|
|
9
10
|
module Protocol
|
|
@@ -14,17 +15,17 @@ module Protocol
|
|
|
14
15
|
HEADER_PATTERN = /\A([!-9;-~]+):[ \t]*([^\x00-\x08\x0a-\x1f\x7f]*)\z/.freeze
|
|
15
16
|
private_constant :HEADER_PATTERN
|
|
16
17
|
|
|
17
|
-
# The
|
|
18
|
-
|
|
18
|
+
# The preamble size limit.
|
|
19
|
+
PREAMBLE_SIZE_LIMIT = 64 * 1024
|
|
19
20
|
|
|
20
|
-
# The
|
|
21
|
-
|
|
21
|
+
# The header size limit for each part.
|
|
22
|
+
HEADER_SIZE_LIMIT = 64 * 1024
|
|
22
23
|
|
|
23
|
-
# The
|
|
24
|
-
|
|
24
|
+
# The header count limit for each part.
|
|
25
|
+
HEADER_COUNT_LIMIT = 64
|
|
25
26
|
|
|
26
|
-
# The
|
|
27
|
-
|
|
27
|
+
# The part count limit.
|
|
28
|
+
PART_COUNT_LIMIT = 128
|
|
28
29
|
|
|
29
30
|
# Represents a single part within a multipart message.
|
|
30
31
|
class Part
|
|
@@ -163,12 +164,12 @@ module Protocol
|
|
|
163
164
|
#
|
|
164
165
|
# @parameter readable [IO, IO::Stream] The readable stream containing multipart data.
|
|
165
166
|
# @parameter boundary [String] The boundary string that separates the parts.
|
|
166
|
-
# @parameter
|
|
167
|
-
# @parameter
|
|
168
|
-
# @parameter
|
|
169
|
-
# @parameter
|
|
170
|
-
def initialize(readable, boundary,
|
|
171
|
-
limits = [
|
|
167
|
+
# @parameter preamble_size_limit [Integer | Nil] The preamble size limit, or nil for no limit.
|
|
168
|
+
# @parameter header_size_limit [Integer | Nil] The header size limit per part, or nil for no limit.
|
|
169
|
+
# @parameter header_count_limit [Integer | Nil] The header count limit per part, or nil for no limit.
|
|
170
|
+
# @parameter part_count_limit [Integer | Nil] The part count limit, or nil for no limit.
|
|
171
|
+
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)
|
|
172
|
+
limits = [preamble_size_limit, header_size_limit, header_count_limit, part_count_limit]
|
|
172
173
|
|
|
173
174
|
if limits.any?{|limit| limit and limit < 0}
|
|
174
175
|
raise ArgumentError, "Multipart limits must be non-negative!"
|
|
@@ -176,10 +177,10 @@ module Protocol
|
|
|
176
177
|
|
|
177
178
|
@readable = IO::Stream(readable)
|
|
178
179
|
@boundary = boundary
|
|
179
|
-
@
|
|
180
|
-
@
|
|
181
|
-
@
|
|
182
|
-
@
|
|
180
|
+
@preamble_size_limit = preamble_size_limit
|
|
181
|
+
@header_size_limit = header_size_limit
|
|
182
|
+
@header_count_limit = header_count_limit
|
|
183
|
+
@part_count_limit = part_count_limit
|
|
183
184
|
|
|
184
185
|
@boundary_marker = "--#{@boundary}\r\n".freeze
|
|
185
186
|
end
|
|
@@ -195,12 +196,12 @@ module Protocol
|
|
|
195
196
|
|
|
196
197
|
# Read lines until we find the first boundary:
|
|
197
198
|
while true
|
|
198
|
-
if line = read_line(preamble_size, @
|
|
199
|
+
if line = read_line(preamble_size, @preamble_size_limit, allowance: @boundary_marker.bytesize, chomp: false)
|
|
199
200
|
if line == @boundary_marker
|
|
200
201
|
break
|
|
201
202
|
else
|
|
202
203
|
preamble_size += line.bytesize
|
|
203
|
-
check_limit(:preamble_size, preamble_size, @
|
|
204
|
+
check_limit(:preamble_size, preamble_size, @preamble_size_limit)
|
|
204
205
|
end
|
|
205
206
|
else
|
|
206
207
|
# End of stream reached without finding boundary:
|
|
@@ -212,7 +213,7 @@ module Protocol
|
|
|
212
213
|
|
|
213
214
|
while true
|
|
214
215
|
part_count += 1
|
|
215
|
-
check_limit(:part_count, part_count, @
|
|
216
|
+
check_limit(:part_count, part_count, @part_count_limit)
|
|
216
217
|
|
|
217
218
|
part = read_part
|
|
218
219
|
break unless part
|
|
@@ -234,18 +235,18 @@ module Protocol
|
|
|
234
235
|
|
|
235
236
|
private
|
|
236
237
|
|
|
237
|
-
def read_line(size,
|
|
238
|
-
if
|
|
239
|
-
limit =
|
|
238
|
+
def read_line(size, limit, allowance: 0, chomp:)
|
|
239
|
+
if limit
|
|
240
|
+
limit = limit - size + allowance + 1
|
|
240
241
|
return @readable.gets("\r\n", limit, chomp: chomp)
|
|
241
242
|
else
|
|
242
243
|
return @readable.gets("\r\n", chomp: chomp)
|
|
243
244
|
end
|
|
244
245
|
end
|
|
245
246
|
|
|
246
|
-
def check_limit(name, value,
|
|
247
|
-
if
|
|
248
|
-
raise
|
|
247
|
+
def check_limit(name, value, limit)
|
|
248
|
+
if limit and value > limit
|
|
249
|
+
raise LimitError, "Multipart #{name} exceeded limit of #{limit}!"
|
|
249
250
|
end
|
|
250
251
|
end
|
|
251
252
|
|
|
@@ -255,18 +256,18 @@ module Protocol
|
|
|
255
256
|
header_count = 0
|
|
256
257
|
|
|
257
258
|
# Read headers until empty line
|
|
258
|
-
while line = read_line(header_size, @
|
|
259
|
+
while line = read_line(header_size, @header_size_limit, allowance: 2, chomp: true)
|
|
259
260
|
if line.empty?
|
|
260
261
|
break # End of headers
|
|
261
262
|
end
|
|
262
263
|
|
|
263
264
|
header_size += line.bytesize + 2
|
|
264
|
-
check_limit(:header_size, header_size, @
|
|
265
|
+
check_limit(:header_size, header_size, @header_size_limit)
|
|
265
266
|
|
|
266
267
|
if match = line.match(HEADER_PATTERN)
|
|
267
268
|
# Parse header line (name: value)
|
|
268
269
|
header_count += 1
|
|
269
|
-
check_limit(:header_count, header_count, @
|
|
270
|
+
check_limit(:header_count, header_count, @header_count_limit)
|
|
270
271
|
|
|
271
272
|
fields << [match[1], match[2].strip]
|
|
272
273
|
else
|
data/lib/protocol/multipart.rb
CHANGED
data/readme.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
Please see the [project releases](https://socketry.github.io/protocol-multipart/releases/index) for all releases.
|
|
8
8
|
|
|
9
|
+
### v0.5.0
|
|
10
|
+
|
|
11
|
+
- Add `Protocol::Multipart::LimitError` for configured processing limits.
|
|
12
|
+
|
|
13
|
+
### v0.4.0
|
|
14
|
+
|
|
15
|
+
- Use consistent limit naming for multipart parser constraints.
|
|
16
|
+
|
|
9
17
|
### v0.3.0
|
|
10
18
|
|
|
11
19
|
- Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.5.0
|
|
4
|
+
|
|
5
|
+
- Add `Protocol::Multipart::LimitError` for configured processing limits.
|
|
6
|
+
|
|
7
|
+
## v0.4.0
|
|
8
|
+
|
|
9
|
+
- Use consistent limit naming for multipart parser constraints.
|
|
10
|
+
|
|
3
11
|
## v0.3.0
|
|
4
12
|
|
|
5
13
|
- Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-multipart
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0.
|
|
75
|
+
version: '0.9'
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0.
|
|
82
|
+
version: '0.9'
|
|
83
83
|
executables: []
|
|
84
84
|
extensions: []
|
|
85
85
|
extra_rdoc_files: []
|
|
@@ -87,6 +87,7 @@ files:
|
|
|
87
87
|
- lib/protocol/multipart.rb
|
|
88
88
|
- lib/protocol/multipart/boundary.rb
|
|
89
89
|
- lib/protocol/multipart/byte_limit.rb
|
|
90
|
+
- lib/protocol/multipart/error.rb
|
|
90
91
|
- lib/protocol/multipart/escape.rb
|
|
91
92
|
- lib/protocol/multipart/form_data.rb
|
|
92
93
|
- lib/protocol/multipart/form_data/parser.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|