Fixed case in MultiPart.Parser where a small chunk contains part of the boundary. Added and fixed related tests. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
f8601750f3
commit
2d51170be2
|
@ -405,7 +405,7 @@ public class MultiPart
|
|||
@Override
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
try (AutoLock l = lock.lock())
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (closed)
|
||||
return null;
|
||||
|
@ -431,7 +431,7 @@ public class MultiPart
|
|||
public void fail(Throwable t)
|
||||
{
|
||||
List<Content.Source> contentSourcesToFail;
|
||||
try (AutoLock l = lock.lock())
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
closed = true;
|
||||
content.forEach(Content.Chunk::release);
|
||||
|
@ -1329,7 +1329,7 @@ public class MultiPart
|
|||
{
|
||||
if (boundaryMatch == boundaryFinder.getLength())
|
||||
{
|
||||
// The boundary was fully matched.
|
||||
// The boundary was fully matched, so the part is complete.
|
||||
buffer.position(buffer.position() + boundaryMatch - partialBoundaryMatch);
|
||||
partialBoundaryMatch = 0;
|
||||
crContent = false;
|
||||
|
@ -1339,7 +1339,8 @@ public class MultiPart
|
|||
}
|
||||
else
|
||||
{
|
||||
// The boundary was partially matched.
|
||||
// The boundary was partially matched, but it
|
||||
// is not clear yet if it is content or boundary.
|
||||
buffer.position(buffer.limit());
|
||||
partialBoundaryMatch = boundaryMatch;
|
||||
return false;
|
||||
|
@ -1347,8 +1348,8 @@ public class MultiPart
|
|||
}
|
||||
else
|
||||
{
|
||||
// There was a partial boundary match, but a mismatch was found.
|
||||
// Handle the special case of parts with no content.
|
||||
// There was a partial boundary match in the previous chunk, but now a
|
||||
// mismatch was found. Handle the special case of parts with no content.
|
||||
if (state == State.CONTENT_START)
|
||||
{
|
||||
// There is some content, reset the boundary match.
|
||||
|
@ -1369,16 +1370,19 @@ public class MultiPart
|
|||
|
||||
// Search for a full boundary.
|
||||
int boundaryOffset = boundaryFinder.match(buffer);
|
||||
if (boundaryOffset == 0)
|
||||
crContent = false;
|
||||
if (boundaryOffset >= 0)
|
||||
{
|
||||
// Output as content the previous partial match, if any.
|
||||
if (boundaryOffset == 0)
|
||||
crContent = false;
|
||||
|
||||
// Emit as content the last CR byte of the previous chunk, if any.
|
||||
notifyCRContent();
|
||||
|
||||
// Emit as content the bytes of this chunk that are before the boundary.
|
||||
int position = buffer.position();
|
||||
int length = boundaryOffset;
|
||||
// BoundaryFinder is configured to search for '\n--Boundary';
|
||||
// if we found '\r\n--Boundary' then the '\r' is not content.
|
||||
// if '\r\n--Boundary' is found, then the '\r' is not content.
|
||||
if (length > 0 && buffer.get(position + length - 1) == '\r')
|
||||
--length;
|
||||
Content.Chunk content = asSlice(chunk, position, length, true);
|
||||
|
@ -1390,34 +1394,25 @@ public class MultiPart
|
|||
|
||||
// Search for a partial boundary at the end of the buffer.
|
||||
partialBoundaryMatch = boundaryFinder.endsWith(buffer);
|
||||
if (partialBoundaryMatch > 0)
|
||||
if (partialBoundaryMatch > 0 && partialBoundaryMatch == buffer.remaining())
|
||||
{
|
||||
int limit = buffer.limit();
|
||||
int sliceLimit = limit - partialBoundaryMatch;
|
||||
// BoundaryFinder is configured to search for '\n--Boundary';
|
||||
// if we found '\r\n--Bo' then the '\r' may not be content,
|
||||
// but remember it in case there is a boundary mismatch.
|
||||
if (sliceLimit > 0 && buffer.get(sliceLimit - 1) == '\r')
|
||||
{
|
||||
crContent = true;
|
||||
--sliceLimit;
|
||||
}
|
||||
int position = buffer.position();
|
||||
Content.Chunk content = asSlice(chunk, position, sliceLimit - position, false);
|
||||
buffer.position(limit);
|
||||
if (content.hasRemaining())
|
||||
notifyPartContent(content);
|
||||
// The boundary was partially matched, but it
|
||||
// is not clear yet if it is content or boundary.
|
||||
buffer.position(buffer.limit());
|
||||
return false;
|
||||
}
|
||||
|
||||
// There is normal content with no boundary.
|
||||
// Here, either the boundary was not found, or it was partially found at the end.
|
||||
|
||||
// Output as content the previous partial match, if any.
|
||||
// Emit as content the last CR byte of the previous chunk, if any.
|
||||
notifyCRContent();
|
||||
// If '\r' is found at the end of the buffer, it may
|
||||
// not be content but the beginning of a '\r\n--Boundary';
|
||||
// remember it in case it is truly normal content.
|
||||
int sliceLimit = buffer.limit();
|
||||
|
||||
// Emit as content the bytes of this chunk, until the partial boundary match (if any).
|
||||
int limit = buffer.limit();
|
||||
int sliceLimit = limit - partialBoundaryMatch;
|
||||
// BoundaryFinder is configured to search for '\n--Boundary';
|
||||
// if '\r\n--Bo' is found, then the '\r' may not be content,
|
||||
// but remember it in case there is a boundary mismatch.
|
||||
if (buffer.get(sliceLimit - 1) == '\r')
|
||||
{
|
||||
crContent = true;
|
||||
|
@ -1425,7 +1420,7 @@ public class MultiPart
|
|||
}
|
||||
int position = buffer.position();
|
||||
Content.Chunk content = asSlice(chunk, position, sliceLimit - position, false);
|
||||
buffer.position(buffer.limit());
|
||||
buffer.position(limit);
|
||||
if (content.hasRemaining())
|
||||
notifyPartContent(content);
|
||||
return false;
|
||||
|
@ -1669,7 +1664,7 @@ public class MultiPart
|
|||
if (value.matches(".??[a-zA-Z]:\\\\[^\\\\].*"))
|
||||
{
|
||||
// Matched incorrectly escaped IE filenames that have the whole
|
||||
// path, as in C:\foo, we just strip any leading & trailing quotes
|
||||
// path, as in C:\foo, just strip any leading & trailing quotes.
|
||||
char first = value.charAt(0);
|
||||
if (first == '"' || first == '\'')
|
||||
value = value.substring(1);
|
||||
|
|
|
@ -663,6 +663,302 @@ public class MultiPartFormDataTest
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkEndsWithLF() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must end with LF.
|
||||
String chunk2 = """
|
||||
DEF
|
||||
""";
|
||||
String terminator = """
|
||||
\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, true, terminator, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n\rDEF\n", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkHasPartialBoundaryMatch() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must have a partial boundary that is actually content.
|
||||
String chunk2 = "\n--AaB0";
|
||||
String terminator = """
|
||||
\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, true, terminator, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n\r\n--AaB0", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkHasPartialBoundaryMatchNextChunkHasRemainingBoundary() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must have a partial boundary that is actually content.
|
||||
String chunk2 = "\n--AaB0";
|
||||
String chunk3 = "3x--\r\n";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, true, chunk3, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkHasPartialBoundaryMatchNextChunksHaveRemainingBoundary() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must have a partial boundary that is actually content.
|
||||
String chunk2 = "\n--AaB0";
|
||||
String chunk3 = "3x";
|
||||
String chunk4 = "--\r\n";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk3, Callback.NOOP);
|
||||
Content.Sink.write(source, true, chunk4, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkHasPartialBoundaryMatchNextChunkHasContent() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must have a partial boundary that is actually content.
|
||||
String chunk2 = "\n--AaB0";
|
||||
String chunk3 = """
|
||||
-CONTENT\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, true, chunk3, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n\r\n--AaB0-CONTENT", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkEndsWithCRNextChunkHasPartialBoundaryMatchNextChunksHaveContent() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// First chunk must end with CR.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Second chunk must have a partial boundary that is actually content.
|
||||
String chunk2 = "\n--AaB";
|
||||
String chunk3 = "03";
|
||||
String chunk4 = """
|
||||
-CONTENT\r
|
||||
--AaB03x--\r
|
||||
""";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk2, Callback.NOOP);
|
||||
Content.Sink.write(source, false, chunk3, Callback.NOOP);
|
||||
Content.Sink.write(source, true, chunk4, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n\r\n--AaB03-CONTENT", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneByteChunks() throws Exception
|
||||
{
|
||||
String boundary = "AaB03x";
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
String form = """
|
||||
--$B\r
|
||||
Content-Disposition: form-data; name="spaces"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC\n\rDEF\n
|
||||
--$B--
|
||||
""".replace("$B", boundary);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
|
||||
new Thread(() ->
|
||||
{
|
||||
ByteBuffer buf = UTF_8.encode(form);
|
||||
while (buf.hasRemaining())
|
||||
{
|
||||
source.write(false, ByteBuffer.wrap(new byte[]{buf.get()}), Callback.NOOP);
|
||||
}
|
||||
source.write(true, BufferUtil.EMPTY_BUFFER, Callback.NOOP);
|
||||
}).start();
|
||||
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part part = parts.get(0);
|
||||
String content = part.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n\rDEF\n", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondChunkIsTerminator() throws Exception
|
||||
{
|
||||
AsyncContent source = new TestContent();
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
|
||||
// This chunk must end with \r.
|
||||
String chunk1 = """
|
||||
--AaB03x\r
|
||||
Content-Disposition: form-data; name="j"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
ABC
|
||||
""" + "\r";
|
||||
// Terminator must start with \n--Boundary.
|
||||
String terminator = """
|
||||
\n--AaB03x--\r
|
||||
""";
|
||||
Content.Sink.write(source, false, chunk1, Callback.NOOP);
|
||||
Content.Sink.write(source, true, terminator, Callback.NOOP);
|
||||
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
|
||||
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
|
||||
{
|
||||
assertEquals(1, parts.size());
|
||||
|
||||
MultiPart.Part spaces = parts.get(0);
|
||||
String content = spaces.getContentAsString(UTF_8);
|
||||
|
||||
assertEquals("ABC\n", content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartWithBackSlashInFileName() throws Exception
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue