Skip to content

Commit 5d40e00

Browse files
committed
Add parameter to StreamingMultipartFormDataParser to indicate if you want to ignore invalid parts.
Also, take this boolean parameter into consideration when deciding what to do when we encounter an invalid part.
1 parent a75a6dd commit 5d40e00

1 file changed

Lines changed: 117 additions & 16 deletions

File tree

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)