Skip to content

Commit 046fab5

Browse files
committed
Merge branch 'release/7.0.0'
2 parents d1a997b + 1768e31 commit 046fab5

File tree

5 files changed

+221
-31
lines changed

5 files changed

+221
-31
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.IO;
2+
using System.Linq;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
namespace HttpMultipartParser.UnitTests.ParserScenarios
8+
{
9+
public class InvalidPart
10+
{
11+
// For details see: https://github.com/Http-Multipart-Data-Parser/Http-Multipart-Data-Parser/issues/110
12+
// This data is considered invalid because it contains nothing but empty lines
13+
private static readonly string _testData = @"--KoZIhvcNAQcB
14+
15+
--KoZIhvcNAQcB--";
16+
17+
private static readonly TestData _testCase = new TestData(
18+
_testData,
19+
Enumerable.Empty<ParameterPart>().ToList(),
20+
Enumerable.Empty<FilePart>().ToList()
21+
);
22+
23+
public InvalidPart()
24+
{
25+
foreach (var filePart in _testCase.ExpectedFileData)
26+
{
27+
filePart.Data.Position = 0;
28+
}
29+
}
30+
31+
[Fact]
32+
public void Exception_is_thrown_when_attempting_to_parse()
33+
{
34+
// The default behavior is to throw an exception when the form contains an invalid section.
35+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
36+
{
37+
Assert.Throws<MultipartParseException>(() => MultipartFormDataParser.Parse(stream));
38+
}
39+
}
40+
41+
[Fact]
42+
public async Task Exception_is_thrown_when_attempting_to_parse_async()
43+
{
44+
// The default behavior is to throw an exception when the form contains an invalid section.
45+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
46+
{
47+
await Assert.ThrowsAsync<MultipartParseException>(() => MultipartFormDataParser.ParseAsync(stream)).ConfigureAwait(false);
48+
}
49+
}
50+
51+
[Fact]
52+
public void Invalid_part_is_ignored()
53+
{
54+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
55+
{
56+
var parser = MultipartFormDataParser.Parse(stream, ignoreInvalidParts: true);
57+
Assert.Equal(0, parser.Files.Count);
58+
Assert.Equal(0, parser.Parameters.Count);
59+
}
60+
}
61+
62+
[Fact]
63+
public async Task Invalid_part_is_ignored_async()
64+
{
65+
using (Stream stream = TestUtil.StringToStream(_testCase.Request, Encoding.UTF8))
66+
{
67+
var parser = await MultipartFormDataParser.ParseAsync(stream, ignoreInvalidParts: true).ConfigureAwait(false);
68+
Assert.Equal(0, parser.Files.Count);
69+
Assert.Equal(0, parser.Parameters.Count);
70+
}
71+
}
72+
}
73+
}

Source/HttpMultipartParser/MultipartFormDataParser.cs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,15 @@ private MultipartFormDataParser()
146146
/// <param name="binaryMimeTypes">
147147
/// List of mimetypes that should be detected as file.
148148
/// </param>
149+
/// <param name="ignoreInvalidParts">
150+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
151+
/// </param>
149152
/// <returns>
150153
/// A new instance of the <see cref="MultipartFormDataParser"/> class.
151154
/// </returns>
152-
public static MultipartFormDataParser Parse(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
155+
public static MultipartFormDataParser Parse(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
153156
{
154-
return Parse(stream, null, encoding, binaryBufferSize, binaryMimeTypes);
157+
return Parse(stream, null, encoding, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts);
155158
}
156159

157160
/// <summary>
@@ -175,13 +178,16 @@ public static MultipartFormDataParser Parse(Stream stream, Encoding encoding, in
175178
/// <param name="binaryMimeTypes">
176179
/// List of mimetypes that should be detected as file.
177180
/// </param>
181+
/// <param name="ignoreInvalidParts">
182+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
183+
/// </param>
178184
/// <returns>
179185
/// A new instance of the <see cref="MultipartFormDataParser"/> class.
180186
/// </returns>
181-
public static MultipartFormDataParser Parse(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
187+
public static MultipartFormDataParser Parse(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
182188
{
183189
var parser = new MultipartFormDataParser();
184-
parser.ParseStream(stream, boundary, encoding, binaryBufferSize, binaryMimeTypes);
190+
parser.ParseStream(stream, boundary, encoding, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts);
185191
return parser;
186192
}
187193

@@ -202,12 +208,15 @@ public static MultipartFormDataParser Parse(Stream stream, string boundary = nul
202208
/// <param name="binaryMimeTypes">
203209
/// List of mimetypes that should be detected as file.
204210
/// </param>
211+
/// <param name="ignoreInvalidParts">
212+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
213+
/// </param>
205214
/// <returns>
206215
/// A new instance of the <see cref="MultipartFormDataParser"/> class.
207216
/// </returns>
208-
public static Task<MultipartFormDataParser> ParseAsync(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
217+
public static Task<MultipartFormDataParser> ParseAsync(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
209218
{
210-
return ParseAsync(stream, null, encoding);
219+
return ParseAsync(stream, null, encoding, DefaultBufferSize, null, ignoreInvalidParts);
211220
}
212221

213222
/// <summary>
@@ -231,13 +240,16 @@ public static Task<MultipartFormDataParser> ParseAsync(Stream stream, Encoding e
231240
/// <param name="binaryMimeTypes">
232241
/// List of mimetypes that should be detected as file.
233242
/// </param>
243+
/// <param name="ignoreInvalidParts">
244+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
245+
/// </param>
234246
/// <returns>
235247
/// A new instance of the <see cref="MultipartFormDataParser"/> class.
236248
/// </returns>
237-
public static async Task<MultipartFormDataParser> ParseAsync(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
249+
public static async Task<MultipartFormDataParser> ParseAsync(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
238250
{
239251
var parser = new MultipartFormDataParser();
240-
await parser.ParseStreamAsync(stream, boundary, encoding, binaryBufferSize, binaryMimeTypes).ConfigureAwait(false);
252+
await parser.ParseStreamAsync(stream, boundary, encoding, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts).ConfigureAwait(false);
241253
return parser;
242254
}
243255

@@ -265,9 +277,12 @@ public static async Task<MultipartFormDataParser> ParseAsync(Stream stream, stri
265277
/// <param name="binaryMimeTypes">
266278
/// List of mimetypes that should be detected as file.
267279
/// </param>
268-
private void ParseStream(Stream stream, string boundary, Encoding encoding, int binaryBufferSize, string[] binaryMimeTypes)
280+
/// <param name="ignoreInvalidParts">
281+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
282+
/// </param>
283+
private void ParseStream(Stream stream, string boundary, Encoding encoding, int binaryBufferSize, string[] binaryMimeTypes, bool ignoreInvalidParts)
269284
{
270-
var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding ?? Encoding.UTF8, binaryBufferSize, binaryMimeTypes);
285+
var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding ?? Encoding.UTF8, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts);
271286
streamingParser.ParameterHandler += parameterPart => _parameters.Add(parameterPart);
272287

273288
streamingParser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber, additionalProperties) =>
@@ -310,9 +325,12 @@ private void ParseStream(Stream stream, string boundary, Encoding encoding, int
310325
/// <param name="binaryMimeTypes">
311326
/// List of mimetypes that should be detected as file.
312327
/// </param>
313-
private async Task ParseStreamAsync(Stream stream, string boundary, Encoding encoding, int binaryBufferSize, string[] binaryMimeTypes)
328+
/// <param name="ignoreInvalidParts">
329+
/// By default the parser will throw an exception if it encounters an invalid part. Set this to true to ignore invalid parts.
330+
/// </param>
331+
private async Task ParseStreamAsync(Stream stream, string boundary, Encoding encoding, int binaryBufferSize, string[] binaryMimeTypes, bool ignoreInvalidParts)
314332
{
315-
var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding ?? Encoding.UTF8, binaryBufferSize, binaryMimeTypes);
333+
var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding ?? Encoding.UTF8, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts);
316334
streamingParser.ParameterHandler += parameterPart => _parameters.Add(parameterPart);
317335

318336
streamingParser.FileHandler += (name, fileName, type, disposition, buffer, bytes, partNumber, additionalProperties) =>

Source/HttpMultipartParser/StreamingMultipartFormDataParser.cs

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,30 @@ public class StreamingMultipartFormDataParser : IStreamingMultipartFormDataParse
5555
/// </remarks>
5656
private const int DefaultBufferSize = 4096;
5757

58+
/// <summary>
59+
/// The mimetypes that are considered a file by default.
60+
/// </summary>
61+
private static readonly string[] DefaultBinaryMimeTypes = { "application/octet-stream" };
62+
5863
#endregion
5964

6065
#region Fields
6166

6267
/// <summary>
6368
/// List of mimetypes that should be detected as file.
6469
/// </summary>
65-
private readonly string[] binaryMimeTypes = { "application/octet-stream" };
70+
private readonly string[] binaryMimeTypes;
6671

6772
/// <summary>
6873
/// The stream we are parsing.
6974
/// </summary>
7075
private readonly Stream stream;
7176

77+
/// <summary>
78+
/// Determines if we should throw an exception when we enconter an invalid part or ignore it.
79+
/// </summary>
80+
private readonly bool ignoreInvalidParts;
81+
7282
/// <summary>
7383
/// The boundary of the multipart message as a string.
7484
/// </summary>
@@ -118,8 +128,11 @@ public class StreamingMultipartFormDataParser : IStreamingMultipartFormDataParse
118128
/// <param name="binaryMimeTypes">
119129
/// List of mimetypes that should be detected as file.
120130
/// </param>
121-
public StreamingMultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
122-
: this(stream, null, encoding, binaryBufferSize, binaryMimeTypes)
131+
/// <param name="ignoreInvalidParts">
132+
/// By default the parser will throw an exception if it encounters an invalid part. set this to true to ignore invalid parts.
133+
/// </param>
134+
public StreamingMultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
135+
: this(stream, null, encoding, binaryBufferSize, binaryMimeTypes, ignoreInvalidParts)
123136
{
124137
}
125138

@@ -144,7 +157,10 @@ public StreamingMultipartFormDataParser(Stream stream, Encoding encoding, int bi
144157
/// <param name="binaryMimeTypes">
145158
/// List of mimetypes that should be detected as file.
146159
/// </param>
147-
public StreamingMultipartFormDataParser(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null)
160+
/// <param name="ignoreInvalidParts">
161+
/// By default the parser will throw an exception if it encounters an invalid part. set this to true to ignore invalid parts.
162+
/// </param>
163+
public StreamingMultipartFormDataParser(Stream stream, string boundary = null, Encoding encoding = null, int binaryBufferSize = DefaultBufferSize, string[] binaryMimeTypes = null, bool ignoreInvalidParts = false)
148164
{
149165
if (stream == null || stream == Stream.Null) { throw new ArgumentNullException(nameof(stream)); }
150166

@@ -153,10 +169,8 @@ public StreamingMultipartFormDataParser(Stream stream, string boundary = null, E
153169
Encoding = encoding ?? Encoding.UTF8;
154170
BinaryBufferSize = binaryBufferSize;
155171
readEndBoundary = false;
156-
if (binaryMimeTypes != null)
157-
{
158-
this.binaryMimeTypes = binaryMimeTypes;
159-
}
172+
this.binaryMimeTypes = binaryMimeTypes ?? DefaultBinaryMimeTypes;
173+
this.ignoreInvalidParts = ignoreInvalidParts;
160174
}
161175

162176
#endregion
@@ -346,24 +360,43 @@ private static async Task<string> DetectBoundaryAsync(RebufferableBinaryReader r
346360
}
347361

348362
/// <summary>
349-
/// Use a few assumptions to determine if a section contains a file or a "data" parameter.
363+
/// Use a few assumptions to determine if a section contains a file.
350364
/// </summary>
351365
/// <param name="parameters">The section parameters.</param>
352366
/// <returns>true if the section contains a file, false otherwise.</returns>
353-
private bool IsFilePart(IDictionary<string, string> parameters)
367+
private bool IsFilePart(IDictionary<string, string> parameters!!)
354368
{
369+
// A section without any parameter is invalid. It is very likely to contain just a bunch of blank lines.
370+
if (parameters.Count == 0) return false;
371+
355372
// If a section contains filename, then it's a file.
356-
if (parameters.ContainsKey("filename")) return true;
373+
else if (parameters.ContainsKey("filename")) return true;
357374

358375
// Check if mimetype is a binary file
359-
else if (parameters.ContainsKey("content-type") &&
360-
binaryMimeTypes.Contains(parameters["content-type"])) return true;
376+
else if (parameters.ContainsKey("content-type") && binaryMimeTypes.Contains(parameters["content-type"])) return true;
361377

362378
// If the section is missing the filename and the name, then it's a file.
363379
// For example, images in an mjpeg stream have neither a name nor a filename.
364380
else if (!parameters.ContainsKey("name")) return true;
365381

366-
// In all other cases, we assume it's a "data" parameter.
382+
// Otherwise this section does not contain a file.
383+
return false;
384+
}
385+
386+
/// <summary>
387+
/// Use a few assumptions to determine if a section contains a "data" parameter.
388+
/// </summary>
389+
/// <param name="parameters">The section parameters.</param>
390+
/// <returns>true if the section contains a data parameter, false otherwise.</returns>
391+
private bool IsParameterPart(IDictionary<string, string> parameters!!)
392+
{
393+
// A section without any parameter is invalid. It is very likely to contain just a bunch of blank lines.
394+
if (parameters.Count == 0) return false;
395+
396+
// A data parameter MUST have a name.
397+
else if (parameters.ContainsKey("name")) return true;
398+
399+
// Otherwise this section does not contain a data parameter.
367400
return false;
368401
}
369402

@@ -950,6 +983,58 @@ private async Task ParseParameterPartAsync(Dictionary<string, string> parameters
950983
ParameterHandler(part);
951984
}
952985

986+
/// <summary>
987+
/// Skip a section of the stream.
988+
/// This is used when a section is deemed to be invalid and the developer has requested to ignore invalid parts.
989+
/// </summary>
990+
/// <param name="reader">
991+
/// The StreamReader to read the data from.
992+
/// </param>
993+
/// <exception cref="MultipartParseException">
994+
/// thrown if unexpected data is found such as running out of stream before hitting the boundary.
995+
/// </exception>
996+
private void SkipPart(RebufferableBinaryReader reader)
997+
{
998+
// Our job is to consume the lines in this section and discard them
999+
string line = reader.ReadLine();
1000+
while (line != boundary && line != endBoundary)
1001+
{
1002+
if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
1003+
line = reader.ReadLine();
1004+
}
1005+
1006+
if (line == endBoundary) readEndBoundary = true;
1007+
}
1008+
1009+
/// <summary>
1010+
/// Asynchronously skip a section of the stream.
1011+
/// This is used when a section is deemed to be invalid and the developer has requested to ignore invalid parts.
1012+
/// </summary>
1013+
/// <param name="reader">
1014+
/// The StreamReader to read the data from.
1015+
/// </param>
1016+
/// <param name="cancellationToken">
1017+
/// The cancellation token.
1018+
/// </param>
1019+
/// <returns>
1020+
/// The asynchronous task.
1021+
/// </returns>
1022+
/// <exception cref="MultipartParseException">
1023+
/// thrown if unexpected data is found such as running out of stream before hitting the boundary.
1024+
/// </exception>
1025+
private async Task SkipPartAsync(RebufferableBinaryReader reader, CancellationToken cancellationToken = default)
1026+
{
1027+
// Our job is to consume the lines in this section and discard them
1028+
string line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
1029+
while (line != boundary && line != endBoundary)
1030+
{
1031+
if (line == null) throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?");
1032+
line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
1033+
}
1034+
1035+
if (line == endBoundary) readEndBoundary = true;
1036+
}
1037+
9531038
/// <summary>
9541039
/// Parses the header of the next section of the multipart stream and
9551040
/// determines if it contains file data or parameter data.
@@ -1029,10 +1114,18 @@ private void ParseSection(RebufferableBinaryReader reader)
10291114
{
10301115
ParseFilePart(parameters, reader);
10311116
}
1032-
else
1117+
else if (IsParameterPart(parameters))
10331118
{
10341119
ParseParameterPart(parameters, reader);
10351120
}
1121+
else if (ignoreInvalidParts)
1122+
{
1123+
SkipPart(reader);
1124+
}
1125+
else
1126+
{
1127+
throw new MultipartParseException("Unable to determine the section type. Some possible reasons include: section is malformed, required parameters such as 'name', 'content-type' or 'filename' are missing, section contains nothing but empty lines.");
1128+
}
10361129
}
10371130

10381131
/// <summary>
@@ -1120,10 +1213,18 @@ private async Task ParseSectionAsync(RebufferableBinaryReader reader, Cancellati
11201213
{
11211214
await ParseFilePartAsync(parameters, reader, cancellationToken).ConfigureAwait(false);
11221215
}
1123-
else
1216+
else if (IsParameterPart(parameters))
11241217
{
11251218
await ParseParameterPartAsync(parameters, reader, cancellationToken).ConfigureAwait(false);
11261219
}
1220+
else if (ignoreInvalidParts)
1221+
{
1222+
await SkipPartAsync(reader).ConfigureAwait(false);
1223+
}
1224+
else
1225+
{
1226+
throw new MultipartParseException("Unable to determine the section type. Some possible reasons include: section is malformed, required parameters such as 'name', 'content-type' or 'filename' are missing, section contains nothing but empty lines.");
1227+
}
11271228
}
11281229

11291230
/// <summary>

0 commit comments

Comments
 (0)