Implemented ranged requests and responses.

Refactored MultiPart so that the different behavior between
multipart/form-data and multipart/byteranges is captured in the code.

Renamed MultiParts to MultiPartFormData, and ServletMultiParts to ServletMultiPartFormData.

Introduced MultiPartByteRanges and used it to implement ranges in ResourceService.
Re-enabled multipart tests for ranges that were disabled, and added more.

Modernized and simplified implementation of ByteRange.
Updated ByteRangeTest.

Removed RangeWriter and its subclasses and their tests, as they are not used anymore.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2022-08-05 13:51:10 +02:00
parent 2e01ed7e08
commit b5332c38a9
29 changed files with 1341 additions and 1575 deletions

View File

@ -13,12 +13,11 @@
package org.eclipse.jetty.client.util;
import java.util.Random;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
/**
@ -43,27 +42,13 @@ import org.eclipse.jetty.io.Content;
* &lt;/form&gt;
* </pre>
*/
public class MultiPartRequestContent extends MultiPart.ContentSource implements Request.Content
public class MultiPartRequestContent extends MultiPartFormData.ContentSource implements Request.Content
{
private static String makeBoundary()
{
Random random = new Random();
StringBuilder builder = new StringBuilder("JettyHttpClientBoundary");
int length = builder.length();
while (builder.length() < length + 16)
{
long rnd = random.nextLong();
builder.append(Long.toString(rnd < 0 ? -rnd : rnd, 36));
}
builder.setLength(length + 16);
return builder.toString();
}
private final String contentType;
public MultiPartRequestContent()
{
this(makeBoundary());
this(MultiPart.generateBoundary("JettyHttpClient-", 24));
}
public MultiPartRequestContent(String boundary)
@ -84,6 +69,7 @@ public class MultiPartRequestContent extends MultiPart.ContentSource implements
HttpFields headers = super.customizePartHeaders(part);
if (headers.contains(HttpHeader.CONTENT_TYPE))
return headers;
Content.Source partContent = part.getContent();
if (partContent instanceof Request.Content requestContent)
{
@ -91,6 +77,7 @@ public class MultiPartRequestContent extends MultiPart.ContentSource implements
if (contentType != null)
return HttpFields.build(headers).put(HttpHeader.CONTENT_TYPE, contentType);
}
return headers;
}
}

View File

@ -34,7 +34,7 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiParts;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
@ -62,7 +62,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts)
protected void process(MultiPartFormData.Parts parts)
{
assertEquals(0, parts.size());
}
@ -88,7 +88,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts)
protected void process(MultiPartFormData.Parts parts)
{
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
@ -119,7 +119,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
@ -157,7 +157,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
@ -209,7 +209,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
@ -265,7 +265,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
@ -317,7 +317,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(2, parts.size());
MultiPart.Part fieldPart = parts.get(0);
@ -364,7 +364,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void process(MultiParts.Parts parts) throws Exception
protected void process(MultiPartFormData.Parts parts) throws Exception
{
assertEquals(2, parts.size());
MultiPart.Part fieldPart = parts.get(0);
@ -422,13 +422,13 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
Path tmpDir = MavenTestingUtils.getTargetTestingPath();
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
assertEquals("multipart/form-data", HttpField.valueParameters(contentType, null));
String boundary = MultiParts.extractBoundary(contentType);
MultiParts multiParts = new MultiParts(boundary);
multiParts.setFilesDirectory(tmpDir);
multiParts.parse(request);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setFilesDirectory(tmpDir);
formData.parse(request);
try
{
process(multiParts.join());
process(formData.join());
response.write(true, BufferUtil.EMPTY_BUFFER, callback);
}
catch (Exception x)
@ -437,6 +437,6 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
}
}
protected abstract void process(MultiParts.Parts parts) throws Exception;
protected abstract void process(MultiPartFormData.Parts parts) throws Exception;
}
}

View File

@ -14,251 +14,185 @@
package org.eclipse.jetty.http;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Byte range inclusive of end points.
* <PRE>
*
* parses the following types of byte ranges:
*
* bytes=100-499
* bytes=-300
* bytes=100-
* bytes=1-2,2-3,6-,-2
*
* given an entity length, converts range to string
*
* bytes 100-499/500
*
* </PRE>
*
* Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2
* <p>
* And yes the spec does strangely say that while 10-20, is bytes 10 to 20 and 10- is bytes 10 until the end that -20 IS NOT bytes 0-20, but the last 20 bytes of the content.
*
* @version $version$
* <p>A representation of a byte range as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7233">RFC 7233</a>.</p>
* <p>This class parses the value of the {@code Range} request header value,
* for example:</p>
* <pre>{@code
* Range: bytes=100-499
* Range: bytes=1-10,5-25,50-,-20
* }</pre>
*/
public class ByteRange
public record ByteRange(long first, long last)
{
private static final Logger LOG = LoggerFactory.getLogger(ByteRange.class);
private long first;
private long last;
public ByteRange(long first, long last)
private ByteRange coalesce(ByteRange r)
{
this.first = first;
this.last = last;
}
public long getFirst()
{
return first;
}
public long getLast()
{
return last;
}
private void coalesce(ByteRange r)
{
first = Math.min(first, r.first);
last = Math.max(last, r.last);
return new ByteRange(Math.min(first, r.first), Math.max(last, r.last));
}
private boolean overlaps(ByteRange range)
{
return (range.first >= this.first && range.first <= this.last) ||
return
// Partial right overlap: 10-20,15-30.
(range.first >= this.first && range.first <= this.last) ||
// Partial left overlap: 20-30,15-25.
(range.last >= this.first && range.last <= this.last) ||
// Full inclusion: 20-30,10-40.
(range.first < this.first && range.last > this.last);
}
public long getSize()
/**
* @return the length of this byte range
*/
public long getLength()
{
return last - first + 1;
}
public String toHeaderRangeString(long size)
/**
* <p>Returns the value for the {@code Content-Range}
* response header corresponding to this byte range.</p>
*
* @param length the content length
* @return the value for the {@code Content-Range} response header for this byte range
*/
public String toHeaderValue(long length)
{
StringBuilder sb = new StringBuilder(40);
sb.append("bytes ");
sb.append(first);
sb.append('-');
sb.append(last);
sb.append("/");
sb.append(size);
return sb.toString();
}
@Override
public int hashCode()
{
return (int)(first ^ last);
}
@Override
public boolean equals(Object obj)
{
if (obj == null)
return false;
if (!(obj instanceof ByteRange))
return false;
return ((ByteRange)obj).first == this.first &&
((ByteRange)obj).last == this.last;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder(60);
sb.append(Long.toString(first));
sb.append(":");
sb.append(Long.toString(last));
return sb.toString();
return "bytes %d-%d/%d".formatted(first, last, length);
}
/**
* @param headers Enumeration of Range header fields.
* @param size Size of the resource.
* @return List of satisfiable ranges
* <p>Parses the {@code Range} header values such as {@code byte=10-20}
* to obtain a list of {@code ByteRange}s.</p>
* <p>Returns an empty list if the parsing fails.</p>
*
* @param headers a list of range values
* @param length the length of the resource for which ranges are requested
* @return a list of {@code ByteRange}s
*/
public static List<ByteRange> satisfiableRanges(Enumeration<String> headers, long size)
public static List<ByteRange> parse(List<String> headers, long length)
{
long end = length - 1;
List<ByteRange> ranges = null;
final long end = size - 1;
// walk through all Range headers
while (headers.hasMoreElements())
String prefix = "bytes=";
for (String header : headers)
{
String header = headers.nextElement();
StringTokenizer tok = new StringTokenizer(header, "=,", false);
String t = null;
try
{
// read all byte ranges for this header
while (tok.hasMoreTokens())
String value = header.trim();
if (!value.startsWith(prefix))
continue;
value = value.substring(prefix.length());
for (String range : value.split(","))
{
try
range = range.trim();
long first = -1;
long last = -1;
int dash = range.indexOf('-');
if (dash < 0 || range.indexOf("-", dash + 1) >= 0)
{
t = tok.nextToken().trim();
if ("bytes".equals(t))
if (LOG.isDebugEnabled())
LOG.debug("bad range format: {}", range);
break;
}
if (dash > 0)
first = Long.parseLong(range.substring(0, dash).trim());
if (dash < (range.length() - 1))
last = Long.parseLong(range.substring(dash + 1).trim());
if (first == -1)
{
if (last == -1)
{
if (LOG.isDebugEnabled())
LOG.debug("bad range format: {}", range);
break;
}
if (last == 0)
continue;
long first = -1;
long last = -1;
int dash = t.indexOf('-');
if (dash < 0 || t.indexOf("-", dash + 1) >= 0)
{
ranges = null;
LOG.warn("Bad range format: {}", t);
break;
}
if (dash > 0)
first = Long.parseLong(t.substring(0, dash).trim());
if (dash < (t.length() - 1))
last = Long.parseLong(t.substring(dash + 1).trim());
if (first == -1)
{
if (last == -1)
{
ranges = null;
LOG.warn("Bad range format: {}", t);
break;
}
if (last == 0)
continue;
// This is a suffix range
first = Math.max(0, size - last);
last = end;
}
else
{
// Range starts after end
if (first >= size)
continue;
if (last == -1)
last = end;
else if (last >= end)
last = end;
}
if (last < first)
{
ranges = null;
LOG.warn("Bad range format: {}", t);
break;
}
ByteRange range = new ByteRange(first, last);
if (ranges == null)
ranges = new ArrayList<>();
boolean coalesced = false;
for (Iterator<ByteRange> i = ranges.listIterator(); i.hasNext(); )
{
ByteRange r = i.next();
if (range.overlaps(r))
{
coalesced = true;
r.coalesce(range);
while (i.hasNext())
{
ByteRange r2 = i.next();
if (r2.overlaps(r))
{
r.coalesce(r2);
i.remove();
}
}
}
}
if (!coalesced)
ranges.add(range);
// This is a suffix range of the form "-20".
first = Math.max(0, end - last + 1);
last = end;
}
catch (NumberFormatException e)
else
{
ranges = null;
LOG.warn("Bad range format: {}", t);
LOG.trace("IGNORED", e);
// Range starts after end.
if (first > end)
continue;
if (last == -1 || last > end)
last = end;
}
if (last < first)
{
if (LOG.isDebugEnabled())
LOG.debug("bad range format: {}", range);
break;
}
ByteRange byteRange = new ByteRange(first, last);
if (ranges == null)
ranges = new ArrayList<>();
ranges.add(byteRange);
}
}
catch (Exception e)
catch (Throwable x)
{
ranges = null;
LOG.warn("Bad range format: {}", t);
LOG.trace("IGNORED", e);
if (LOG.isDebugEnabled())
LOG.debug("could not parse range {}", header, x);
return List.of();
}
}
return ranges;
if (ranges == null)
return List.of();
if (ranges.size() == 1)
return ranges;
// Sort and coalesce in one pass through the list.
List<ByteRange> result = new ArrayList<>();
ranges.sort(Comparator.comparingLong(ByteRange::first));
ByteRange range1 = ranges.get(0);
for (int i = 1; i < ranges.size(); ++i)
{
ByteRange range2 = ranges.get(i);
if (range1.overlaps(range2))
{
range1 = range1.coalesce(range2);
}
else
{
result.add(range1);
range1 = range2;
}
}
result.add(range1);
return result;
}
public static String to416HeaderRangeString(long size)
/**
* <p>Returns the value for the {@code Content-Range} response header
* when the range is non satisfiable and therefore the response status
* code is {@link HttpStatus#RANGE_NOT_SATISFIABLE_416}.</p>
*
* @param length the content length in bytes
* @return the non satisfiable value for the {@code Content-Range} response header
*/
public static String toNonSatisfiableHeaderValue(long length)
{
StringBuilder sb = new StringBuilder(40);
sb.append("bytes */");
sb.append(size);
return sb.toString();
return "bytes */" + length;
}
}

View File

@ -346,17 +346,21 @@ public class HttpTester
return _content.toByteArray();
}
public ByteBuffer getContentByteBuffer()
{
return ByteBuffer.wrap(getContentBytes());
}
public String getContent()
{
if (_content == null)
return null;
byte[] bytes = _content.toByteArray();
String contentType = get(HttpHeader.CONTENT_TYPE);
String encoding = MimeTypes.getCharsetFromContentType(contentType);
Charset charset = encoding == null ? StandardCharsets.UTF_8 : Charset.forName(encoding);
return new String(bytes, charset);
return _content.toString(charset);
}
@Override

View File

@ -23,11 +23,14 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ThreadLocalRandom;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
@ -50,21 +53,65 @@ import static java.nio.charset.StandardCharsets.UTF_8;
/**
* <p>Namespace class for interrelated classes that provide
* support for parsing and generating multipart bytes.</p>
* <p>Most applications should make use of {@link MultiParts}
* as it provides a simpler API.</p>
* <p>Most applications should make use of {@link MultiPartFormData}
* or {@link MultiPartByteRanges} as they provide a simpler API.</p>
* <p>Multipart parsing is provided by {@link Parser}.</p>
* <p>Multipart generation is provided by {@link ContentSource}.</p>
* <p>Multipart generation is provided by {@link AbstractContentSource} and its subclasses.</p>
* <p>A single part of a multipart content is represented by {@link Part}
* and its subclasses.</p>
*
* @see MultiParts
* @see MultiPartFormData
* @see MultiPartByteRanges
*/
public class MultiPart
{
private static final int MAX_BOUNDARY_LENGTH = 70;
private MultiPart()
{
}
/**
* <p>Extracts the value of the {@code boundary} parameter
* from the {@code Content-Type} header value, or returns
* {@code null} if the {@code boundary} parameter is missing.</p>
*
* @param contentType the {@code Content-Type} header value
* @return the value of the {@code boundary} parameter, or
* {@code null} if the {@code boundary} parameter is missing
*/
public static String extractBoundary(String contentType)
{
Map<String, String> parameters = new HashMap<>();
HttpField.valueParameters(contentType, parameters);
return QuotedStringTokenizer.unquote(parameters.get("boundary"));
}
/**
* <p>Generates a multipart boundary, made of the given optional
* prefix string and the given number of random characters.</p>
* <p>The total length of the boundary will be trimmed to at
* most 70 characters, as specified in RFC 2046.</p>
*
* @param prefix a possibly {@code null} prefix
* @param randomLength a number of random characters to add after the prefix
* @return a boundary string
*/
public static String generateBoundary(String prefix, int randomLength)
{
if (prefix == null && randomLength < 1)
throw new IllegalArgumentException("invalid boundary length");
StringBuilder builder = new StringBuilder(prefix == null ? "" : prefix);
int length = builder.length();
while (builder.length() < length + randomLength)
{
long rnd = ThreadLocalRandom.current().nextLong();
builder.append(Long.toString(rnd < 0 ? -rnd : rnd, 36));
}
builder.setLength(Math.min(length + randomLength, MAX_BOUNDARY_LENGTH));
return builder.toString();
}
/**
* <p>A single part of a multipart content.</p>
* <p>A part has an optional name, an optional fileName,
@ -181,7 +228,7 @@ public class MultiPart
*/
public static class ByteBufferPart extends Part
{
private final List<ByteBuffer> content;
private final Content.Source content;
private final long length;
public ByteBufferPart(String name, String fileName, HttpFields fields, ByteBuffer... buffers)
@ -192,14 +239,14 @@ public class MultiPart
public ByteBufferPart(String name, String fileName, HttpFields fields, List<ByteBuffer> content)
{
super(name, fileName, fields);
this.content = content;
this.content = new ByteBufferContentSource(content);
this.length = content.stream().mapToLong(Buffer::remaining).sum();
}
@Override
public Content.Source getContent()
{
return new ByteBufferContentSource(content);
return content;
}
@Override
@ -250,48 +297,40 @@ public class MultiPart
}
/**
* <p>A {@link Part} that holds its content in a file-system path.</p>
* <p>A {@link Part} whose content is in a file.</p>
*/
public static class PathPart extends Part
{
private final Path path;
private final PathContentSource content;
public PathPart(String name, String fileName, HttpFields fields, Path path)
{
super(name, fileName, fields);
this.path = path;
this.content = new PathContentSource(path);
}
public Path getPath()
{
return path;
return content.getPath();
}
@Override
public Content.Source getContent()
{
try
{
return new PathContentSource(path);
}
catch (IOException x)
{
throw new UncheckedIOException(x);
}
return content;
}
@Override
public void writeTo(Path path) throws IOException
{
// TODO: make it more efficient via Files.move().
super.writeTo(path);
Files.move(getPath(), path, StandardCopyOption.REPLACE_EXISTING);
}
public void delete()
{
try
{
Files.delete(path);
Files.delete(getPath());
}
catch (IOException x)
{
@ -348,13 +387,15 @@ public class MultiPart
* <p>An asynchronous {@link Content.Source} where {@link Part}s can
* be added to it to form a multipart content.</p>
* <p>When this {@link Content.Source} is read, it will produce the
* bytes (including boundary separators) in the {@code multipart/form-data}
* format, as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7578">RFC 7578</a>.</p>
* bytes (including boundary separators) in the multipart format.</p>
* <p>Subclasses should override {@link #customizePartHeaders(Part)}
* to produce the right part headers depending on the specific
* multipart subtype (for example, {@code multipart/form-data} or
* {@code multipart/byteranges}, etc.).</p>
* <p>Typical asynchronous usage is the following:</p>
* <pre>{@code
* // Create a ContentSource specifying the boundary string.
* ContentSource source = new ContentSource("my_boundary");
* // Create a ContentSource subclass.
* ContentSource source = ...;
*
* // Add parts to the ContentSource.
* source.addPart(new ByteBufferPart());
@ -378,7 +419,7 @@ public class MultiPart
* chunk when all the parts have been added and this {@code ContentSource}
* has been closed.</p>
*/
public static class ContentSource implements Content.Source, Closeable
public abstract static class AbstractContentSource implements Content.Source, Closeable
{
private final AutoLock lock = new AutoLock();
private final SerializedInvoker invoker = new SerializedInvoker();
@ -395,9 +436,9 @@ public class MultiPart
private Content.Chunk.Error errorChunk;
private Part part;
public ContentSource(String boundary)
public AbstractContentSource(String boundary)
{
if (boundary.isBlank() || boundary.length() > 70)
if (boundary.isBlank() || boundary.length() > MAX_BOUNDARY_LENGTH)
throw new IllegalArgumentException("Invalid boundary: must consists of 1 to 70 characters");
// RFC 2046 requires the boundary to not end with a space.
boundary = boundary.stripTrailing();
@ -485,7 +526,11 @@ public class MultiPart
@Override
public long getLength()
{
// TODO can it be computed?
// TODO: it is difficult to calculate the length because
// we need to allow for customization of the headers from
// subclasses, and then serialize all the headers to get
// their length (handling UTF-8 values) and we don't want
// to do it twice (here and in read()).
return -1;
}
@ -607,17 +652,7 @@ public class MultiPart
protected HttpFields customizePartHeaders(Part part)
{
HttpFields headers = part.getHeaders();
if (headers.contains(HttpHeader.CONTENT_DISPOSITION))
return headers;
String value = "form-data";
String name = part.getName();
if (name != null)
value += "; name=" + QuotedStringTokenizer.quote(name);
String fileName = part.getFileName();
if (fileName != null)
value += "; filename=" + QuotedStringTokenizer.quote(fileName);
return HttpFields.build(headers).put(HttpHeader.CONTENT_DISPOSITION, value);
return part.getHeaders();
}
private void checkPartHeadersLength(Utf8StringBuilder builder)
@ -1378,16 +1413,15 @@ public class MultiPart
/**
* <p>A {@link Parser.Listener} that emits {@link Part} objects.</p>
* <p>The part content is stored in memory.</p>
* <p>Subclasses may just implement {@link #onPart(Part)} to
* receive the parts of the multipart content.</p>
* <p>Subclasses implement {@link #onPartContent(Content.Chunk)}
* and {@link #onPart(String, String, HttpFields)} to create the
* parts of the multipart content.</p>
*/
public abstract static class AbstractPartsListener implements Parser.Listener
{
private static final Logger LOG = LoggerFactory.getLogger(AbstractPartsListener.class);
private final HttpFields.Mutable fields = HttpFields.build();
private final List<Content.Chunk> content = new ArrayList<>();
private String name;
private String fileName;
@ -1401,11 +1435,6 @@ public class MultiPart
return fileName;
}
public List<Content.Chunk> getContent()
{
return content;
}
@Override
public void onPartHeader(String headerName, String headerValue)
{
@ -1460,46 +1489,36 @@ public class MultiPart
}
}
@Override
public void onPartContent(Content.Chunk chunk)
{
if (LOG.isDebugEnabled())
LOG.debug("part content last={} {}", chunk.isLast(), BufferUtil.toDetailString(chunk.getByteBuffer()));
content.add(chunk);
}
@Override
public void onPartEnd()
{
Part part = newPart(getName(), getFileName(), fields.takeAsImmutable(), List.copyOf(content));
content.clear();
name = null;
fileName = null;
notifyPart(part);
String name = getName();
this.name = null;
String fileName = getFileName();
this.fileName = null;
HttpFields headers = fields.takeAsImmutable();
notifyPart(name, fileName, headers);
}
/**
* <p>Callback method invoked when a {@link Part} has been parsed.</p>
*
* @param part the {@link Part} that has been parsed
* @param name the part name
* @param fileName the part fileName
* @param headers the part headers
*/
public abstract void onPart(Part part);
public abstract void onPart(String name, String fileName, HttpFields headers);
protected Part newPart(String name, String fileName, HttpFields headers, List<Content.Chunk> content)
{
return new ChunksPart(name, fileName, headers, content);
}
private void notifyPart(Part part)
private void notifyPart(String name, String fileName, HttpFields headers)
{
try
{
onPart(part);
onPart(name, fileName, headers);
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("failure while notifying part {}", part, x);
LOG.debug("failure while notifying part {}", name, x);
}
}
}

View File

@ -0,0 +1,323 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.thread.AutoLock;
/**
* <p>A {@link CompletableFuture} that is completed when a multipart/byteranges
* content has been parsed asynchronously from a {@link Content.Source} via
* {@link #parse(Content.Source)}.</p>
* <p>Once the parsing of the multipart/byteranges content completes successfully,
* objects of this class are completed with a {@link MultiPartByteRanges.Parts}
* object.</p>
* <p>Typical usage:</p>
* <pre>{@code
* // Some headers that include Content-Type.
* HttpFields headers = ...;
* String boundary = MultiPart.extractBoundary(headers.get(HttpHeader.CONTENT_TYPE));
*
* // Some multipart/byteranges content.
* Content.Source content = ...;
*
* // Create and configure MultiPartByteRanges.
* MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
*
* // Parse the content.
* byteRanges.parse(content)
* // When complete, use the parts.
* .thenAccept(parts -> ...);
* }</pre>
*
* @see Parts
*/
public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.Parts>
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
public MultiPartByteRanges(String boundary)
{
this.parser = new MultiPart.Parser(boundary, listener);
}
/**
* @return the boundary string
*/
public String getBoundary()
{
return parser.getBoundary();
}
@Override
public boolean completeExceptionally(Throwable failure)
{
listener.fail(failure);
return super.completeExceptionally(failure);
}
/**
* <p>Parses the given multipart/byteranges content.</p>
* <p>Returns this {@code MultiPartByteRanges} object,
* so that it can be used in the typical "fluent" style
* of {@link CompletableFuture}.</p>
*
* @param content the multipart/byteranges content to parse
* @return this {@code MultiPartByteRanges} object
*/
public MultiPartByteRanges parse(Content.Source content)
{
new Runnable()
{
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = content.read();
if (chunk == null)
{
content.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
listener.onFailure(error.getCause());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast() || isDone())
return;
}
}
}.run();
return this;
}
private void parse(Content.Chunk chunk)
{
if (listener.isFailed())
return;
parser.parse(chunk);
}
/**
* <p>An ordered list of {@link MultiPart.Part}s that can
* be accessed by index, or iterated over.</p>
*/
public static class Parts implements Iterable<MultiPart.Part>
{
private final String boundary;
private final List<MultiPart.Part> parts;
private Parts(String boundary, List<MultiPart.Part> parts)
{
this.boundary = boundary;
this.parts = parts;
}
/**
* @return the boundary string
*/
public String getBoundary()
{
return boundary;
}
/**
* <p>Returns the {@link MultiPart.Part} at the given index, a number
* between {@code 0} included and the value returned by {@link #size()}
* excluded.</p>
*
* @param index the index of the {@code MultiPart.Part} to return
* @return the {@code MultiPart.Part} at the given index
*/
public MultiPart.Part get(int index)
{
return parts.get(index);
}
/**
* @return the number of parts
* @see #get(int)
*/
public int size()
{
return parts.size();
}
@Override
public Iterator<MultiPart.Part> iterator()
{
return parts.iterator();
}
}
/**
* <p>The multipart/byteranges specific content source.</p>
*
* @see MultiPart.AbstractContentSource
*/
public static class ContentSource extends MultiPart.AbstractContentSource
{
public ContentSource(String boundary)
{
super(boundary);
}
@Override
public boolean addPart(MultiPart.Part part)
{
if (part instanceof Part)
return super.addPart(part);
return false;
}
}
/**
* <p>A specialized {@link org.eclipse.jetty.io.content.PathContentSource}
* whose content is sliced by a byte range.</p>
*/
public static class PathContentSource extends org.eclipse.jetty.io.content.PathContentSource
{
private final ByteRange byteRange;
public PathContentSource(Path path, ByteRange byteRange)
{
super(path);
this.byteRange = byteRange;
}
@Override
protected SeekableByteChannel open() throws IOException
{
SeekableByteChannel channel = super.open();
channel.position(byteRange.first());
return channel;
}
@Override
protected int read(SeekableByteChannel channel, ByteBuffer byteBuffer) throws IOException
{
int read = super.read(channel, byteBuffer);
if (read < 0)
return read;
read = (int)Math.min(read, byteRange.getLength());
byteBuffer.position(read);
return read;
}
@Override
protected boolean isReadComplete(long read)
{
return read == byteRange.getLength();
}
}
/**
* <p>A {@link MultiPart.Part} whose content is a byte range of a file.</p>
*/
public static class Part extends MultiPart.Part
{
private final PathContentSource content;
public Part(String contentType, Path path, ByteRange byteRange)
{
this(HttpFields.build().put(HttpHeader.CONTENT_TYPE, contentType), path, byteRange);
}
public Part(HttpFields headers, Path path, ByteRange byteRange)
{
super(null, null, headers);
content = new PathContentSource(path, byteRange);
}
@Override
public Content.Source getContent()
{
return content;
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
{
private final AutoLock lock = new AutoLock();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private final List<MultiPart.Part> parts = new ArrayList<>();
private Throwable failure;
private boolean isFailed()
{
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
}
@Override
public void onPartContent(Content.Chunk chunk)
{
partChunks.add(chunk);
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
partChunks.clear();
}
@Override
public void onComplete()
{
super.onComplete();
complete(new Parts(getBoundary(), parts));
}
@Override
public void onFailure(Throwable failure)
{
super.onFailure(failure);
completeExceptionally(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> toFail;
try (AutoLock ignored = lock.lock())
{
if (failure != null)
return;
failure = cause;
toFail = new ArrayList<>(parts);
parts.clear();
}
toFail.forEach(part -> part.getContent().fail(cause));
}
}
}

View File

@ -22,10 +22,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@ -49,44 +47,29 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
* <pre>{@code
* // Some headers that include Content-Type.
* HttpFields headers = ...;
* String boundary = MultiParts.extractBoundary(headers.get(HttpHeader.CONTENT_TYPE));
* String boundary = MultiPart.extractBoundary(headers.get(HttpHeader.CONTENT_TYPE));
*
* // Some multipart/form-data content.
* Content.Source content = ...;
*
* // Create and configure MultiParts.
* MultiParts multiParts = new MultiParts(boundary);
* // Create and configure MultiPartFormData.
* MultiPartFormData formData = new MultiPartFormData(boundary);
* // Where to store the files.
* multiParts.setFilesDirectory(Path.of("/tmp"));
* formData.setFilesDirectory(Path.of("/tmp"));
* // Max 1 MiB files.
* multiParts.setMaxFileSize(1024 * 1024);
* formData.setMaxFileSize(1024 * 1024);
*
* // Parse the content.
* multiParts.parse(content)
* formData.parse(content)
* // When complete, use the parts.
* .thenAccept(parts -> ...);
* }</pre>
*
* @see Parts
*/
public class MultiParts extends CompletableFuture<MultiParts.Parts>
public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts>
{
private static final Logger LOG = LoggerFactory.getLogger(MultiParts.class);
/**
* <p>Extracts the value of the {@code boundary} parameter
* from the {@code Content-Type} header value.</p>
*
* @param contentType the {@code Content-Type} header value
* @return the value of the {@code boundary} parameter
*/
public static String extractBoundary(String contentType)
{
Map<String, String> parameters = new HashMap<>();
HttpField.valueParameters(contentType, parameters);
String boundary = QuotedStringTokenizer.unquote(parameters.get("boundary"));
return boundary != null ? boundary : "";
}
private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormData.class);
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
@ -97,7 +80,7 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
private long maxLength = -1;
private long length;
public MultiParts(String boundary)
public MultiPartFormData(String boundary)
{
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
}
@ -112,13 +95,14 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
/**
* <p>Parses the given multipart/form-data content.</p>
* <p>Returns this {@code MultiParts} object, so that it can be used
* in the typical "fluent" style of {@link CompletableFuture}.</p>
* <p>Returns this {@code MultiPartFormData} object,
* so that it can be used in the typical "fluent"
* style of {@link CompletableFuture}.</p>
*
* @param content the multipart/form-data content to parse
* @return this {@code MultiParts} object
* @return this {@code MultiPartFormData} object
*/
public MultiParts parse(Content.Source content)
public MultiPartFormData parse(Content.Source content)
{
new Runnable()
{
@ -135,12 +119,12 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
}
if (chunk instanceof Content.Chunk.Error error)
{
completeExceptionally(error.getCause());
listener.onFailure(error.getCause());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast())
if (chunk.isLast() || isDone())
return;
}
}
@ -380,31 +364,46 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
{
return parts.iterator();
}
}
/**
* <p>Returns a new {@link MultiPart.ContentSource} with the same boundary
* as this object, and containing all the parts contained in this object.</p>
*
* @return a new {@link MultiPart.ContentSource} with all the parts of this object
*/
public MultiPart.ContentSource toContentSource()
/**
* <p>The multipart/form-data specific content source.</p>
*
* @see MultiPart.AbstractContentSource
*/
public static class ContentSource extends MultiPart.AbstractContentSource
{
public ContentSource(String boundary)
{
MultiPart.ContentSource result = new MultiPart.ContentSource(getBoundary());
parts.forEach(result::addPart);
result.close();
return result;
super(boundary);
}
protected HttpFields customizePartHeaders(MultiPart.Part part)
{
HttpFields headers = super.customizePartHeaders(part);
if (headers.contains(HttpHeader.CONTENT_DISPOSITION))
return headers;
String value = "form-data";
String name = part.getName();
if (name != null)
value += "; name=" + QuotedStringTokenizer.quote(name);
String fileName = part.getFileName();
if (fileName != null)
value += "; filename=" + QuotedStringTokenizer.quote(fileName);
return HttpFields.build(headers).put(HttpHeader.CONTENT_DISPOSITION, value);
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
{
private final AutoLock lock = new AutoLock();
private Throwable failure;
private final List<MultiPart.Part> parts = new ArrayList<>();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private long fileSize;
private long memoryFileSize;
private Path filePath;
private volatile SeekableByteChannel fileChannel;
private SeekableByteChannel fileChannel;
private Throwable failure;
@Override
public void onPartContent(Content.Chunk chunk)
@ -432,7 +431,7 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
if (ensureFileChannel())
{
// Write existing memory chunks.
for (Content.Chunk c : getContent())
for (Content.Chunk c : partChunks)
{
if (!write(c.getByteBuffer()))
return;
@ -446,7 +445,7 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
}
}
}
super.onPartContent(chunk);
partChunks.add(chunk);
}
private boolean write(ByteBuffer buffer)
@ -485,22 +484,19 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
}
@Override
protected MultiPart.Part newPart(String name, String fileName, HttpFields headers, List<Content.Chunk> content)
public void onPart(String name, String fileName, HttpFields headers)
{
MultiPart.Part part;
if (fileChannel != null)
return new MultiPart.PathPart(name, fileName, headers, filePath);
part = new MultiPart.PathPart(name, fileName, headers, filePath);
else
return super.newPart(name, fileName, headers, content);
}
@Override
public void onPart(MultiPart.Part part)
{
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
// Reset part-related state.
fileSize = 0;
memoryFileSize = 0;
filePath = null;
fileChannel = null;
partChunks.clear();
// Store the new part.
try (AutoLock ignored = lock.lock())
{
@ -532,17 +528,21 @@ public class MultiParts extends CompletableFuture<MultiParts.Parts>
private void fail(Throwable cause)
{
List<MultiPart.Part> toFail;
try (AutoLock ignored = lock.lock())
{
if (failure == null)
{
failure = cause;
parts.stream()
.filter(part -> part instanceof MultiPart.PathPart)
.map(MultiPart.PathPart.class::cast)
.forEach(MultiPart.PathPart::delete);
parts.clear();
}
if (failure != null)
return;
failure = cause;
toFail = new ArrayList<>(parts);
parts.clear();
}
for (MultiPart.Part part : toFail)
{
if (part instanceof MultiPart.PathPart pathPart)
pathPart.delete();
else
part.getContent().fail(cause);
}
close();
delete();

View File

@ -13,68 +13,56 @@
package org.eclipse.jetty.http;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
public class ByteRangeTest
{
private void assertInvalidRange(String rangeString)
{
Vector<String> strings = new Vector<>();
strings.add(rangeString);
List<ByteRange> ranges = ByteRange.satisfiableRanges(strings.elements(), 200);
assertNull(ranges, "Invalid Range [" + rangeString + "] should result in no satisfiable ranges");
List<String> strings = List.of(rangeString);
List<ByteRange> ranges = ByteRange.parse(strings, 200);
assertNotNull(ranges);
assertThat("Invalid Range [" + rangeString + "] should result in no satisfiable ranges", ranges.size(), is(0));
}
private void assertRange(String msg, int expectedFirst, int expectedLast, int size, ByteRange actualRange)
{
assertEquals(expectedFirst, actualRange.getFirst(), msg + " - first");
assertEquals(expectedLast, actualRange.getLast(), msg + " - last");
assertEquals(expectedFirst, actualRange.first(), msg + " - first");
assertEquals(expectedLast, actualRange.last(), msg + " - last");
String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size);
assertThat(msg + " - header range string", actualRange.toHeaderRangeString(size), is(expectedHeader));
assertThat(msg + " - header range string", actualRange.toHeaderValue(size), is(expectedHeader));
}
private void assertSimpleRange(int expectedFirst, int expectedLast, String rangeId, int size)
{
ByteRange range = parseRange(rangeId, size);
assertEquals(expectedFirst, range.getFirst(), "Range [" + rangeId + "] - first");
assertEquals(expectedLast, range.getLast(), "Range [" + rangeId + "] - last");
assertEquals(expectedFirst, range.first(), "Range [" + rangeId + "] - first");
assertEquals(expectedLast, range.last(), "Range [" + rangeId + "] - last");
String expectedHeader = String.format("bytes %d-%d/%d", expectedFirst, expectedLast, size);
assertEquals(expectedHeader, range.toHeaderRangeString(size), "Range [" + rangeId + "] - header range string");
assertEquals(expectedHeader, range.toHeaderValue(size), "Range [" + rangeId + "] - header range string");
}
private ByteRange parseRange(String rangeString, int size)
{
Vector<String> strings = new Vector<>();
strings.add(rangeString);
List<ByteRange> ranges = ByteRange.satisfiableRanges(strings.elements(), size);
List<String> strings = List.of(rangeString);
List<ByteRange> ranges = ByteRange.parse(strings, size);
assertNotNull(ranges, "Satisfiable Ranges should not be null");
assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
return ranges.iterator().next();
return ranges.get(0);
}
private List<ByteRange> parseRanges(int size, String... rangeString)
{
Vector<String> strings = new Vector<>();
for (String range : rangeString)
{
strings.add(range);
}
List<ByteRange> ranges = ByteRange.satisfiableRanges(strings.elements(), size);
List<String> strings = List.of(rangeString);
List<ByteRange> ranges = ByteRange.parse(strings, size);
assertNotNull(ranges, "Satisfiable Ranges should not be null");
return ranges;
}
@ -82,17 +70,17 @@ public class ByteRangeTest
@Test
public void testHeader416RangeString()
{
assertEquals("bytes */100", ByteRange.to416HeaderRangeString(100), "416 Header on size 100");
assertEquals("bytes */123456789", ByteRange.to416HeaderRangeString(123456789), "416 Header on size 123456789");
assertEquals("bytes */100", ByteRange.toNonSatisfiableHeaderValue(100), "416 Header on size 100");
assertEquals("bytes */123456789", ByteRange.toNonSatisfiableHeaderValue(123456789), "416 Header on size 123456789");
}
@Test
public void testInvalidRanges()
{
// Invalid if parsing "Range" header
assertInvalidRange("bytes=a-b"); // letters invalid
assertInvalidRange("byte=10-3"); // key is bad
assertInvalidRange("onceuponatime=5-10"); // key is bad
assertInvalidRange("bytes=a-b"); // letters are invalid
assertInvalidRange("byte=10-3"); // unit is bad and values wrong
assertInvalidRange("onceuponatime=5-10"); // unit is bad
assertInvalidRange("bytes=300-310"); // outside of size (200)
}
@ -103,15 +91,12 @@ public class ByteRangeTest
public void testMultipleAbsoluteRanges()
{
int size = 50;
String rangeString;
rangeString = "bytes=5-20,35-65";
String rangeString = "bytes=5-20,35-65";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 20, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 35, 49, size, ranges.get(1));
}
/**
@ -124,9 +109,8 @@ public class ByteRangeTest
List<ByteRange> ranges = parseRanges(size, "bytes=5-20", "bytes=35-65");
assertEquals(2, ranges.size());
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("testMultipleAbsoluteRangesSplit[0]", 5, 20, size, inclusiveByteRangeIterator.next());
assertRange("testMultipleAbsoluteRangesSplit[1]", 35, 49, size, inclusiveByteRangeIterator.next());
assertRange("testMultipleAbsoluteRangesSplit[0]", 5, 20, size, ranges.get(0));
assertRange("testMultipleAbsoluteRangesSplit[1]", 35, 49, size, ranges.get(1));
}
/**
@ -136,113 +120,94 @@ public class ByteRangeTest
public void testMultipleRangesClipped()
{
int size = 50;
String rangeString;
rangeString = "bytes=5-20,35-65,-5";
String rangeString = "bytes=5-20,35-65,-5";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 20, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 35, 49, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 20, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 35, 49, size, ranges.get(1));
}
@Test
public void testMultipleRangesOverlapping()
{
int size = 200;
String rangeString;
rangeString = "bytes=5-20,15-25";
String rangeString = "bytes=5-20,15-25";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 25, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 25, size, ranges.get(0));
}
@Test
public void testMultipleRangesSplit()
{
int size = 200;
String rangeString;
rangeString = "bytes=5-10,15-20";
String rangeString = "bytes=5-10,15-20";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 10, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 15, 20, size, ranges.get(1));
}
@Test
public void testMultipleSameRangesSplit()
{
int size = 200;
String rangeString;
rangeString = "bytes=5-10,15-20,5-10,15-20,5-10,5-10,5-10,5-10,5-10,5-10";
String rangeString = "bytes=5-10,15-20,5-10,15-20,5-10,5-10,5-10,5-10,5-10,5-10";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 10, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 15, 20, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 10, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 15, 20, size, ranges.get(1));
}
@Test
public void testMultipleOverlappingRanges()
{
int size = 200;
String rangeString;
rangeString = "bytes=5-15,20-30,10-25";
String rangeString = "bytes=5-15,20-30,10-25";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(1, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 5, 30, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 5, 30, size, ranges.get(0));
}
@Test
public void testMultipleOverlappingRangesOrdered()
{
int size = 200;
String rangeString;
rangeString = "bytes=20-30,5-15,0-5,25-35";
String rangeString = "bytes=20-30,5-15,0-5,25-35";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 0, 15, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 20, 35, size, ranges.get(1));
}
@Test
public void testMultipleOverlappingRangesOrderedSplit()
{
int size = 200;
String rangeString;
rangeString = "bytes=20-30,5-15,0-5,25-35";
String rangeString = "bytes=20-30,5-15,0-5,25-35";
List<ByteRange> ranges = parseRanges(size, "bytes=20-30", "bytes=5-15", "bytes=0-5,25-35");
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 20, 35, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 0, 15, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 0, 15, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 20, 35, size, ranges.get(1));
}
@Test
public void testNasty()
{
int size = 200;
String rangeString;
rangeString = "bytes=90-100, 10-20, 30-40, -161";
String rangeString = "bytes=90-100, 10-20, 30-40, -161";
List<ByteRange> ranges = parseRanges(size, rangeString);
assertEquals(2, ranges.size(), "Satisfiable Ranges of [" + rangeString + "] count");
Iterator<ByteRange> inclusiveByteRangeIterator = ranges.iterator();
assertRange("Range [" + rangeString + "]", 30, 199, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 10, 20, size, inclusiveByteRangeIterator.next());
assertRange("Range [" + rangeString + "]", 10, 20, size, ranges.get(0));
assertRange("Range [" + rangeString + "]", 30, 199, size, ranges.get(1));
}
@Test
@ -265,12 +230,11 @@ public class ByteRangeTest
// TODO: evaluate this vs assertInvalidRange() above, which behavior is correct? null? or empty list?
private void assertBadRangeList(int size, String badRange)
{
Vector<String> strings = new Vector<>();
strings.add(badRange);
List<ByteRange> ranges = ByteRange.satisfiableRanges(strings.elements(), size);
// if one part is bad, the entire set of ranges should be treated as bad, per RFC7233
assertThat("Should have no ranges", ranges, is(nullValue()));
List<String> strings = List.of(badRange);
List<ByteRange> ranges = ByteRange.parse(strings, size);
// If one part is bad, the entire set of ranges should be treated as bad, per RFC7233.
assertNotNull(ranges);
assertThat("Should have no ranges", ranges.size(), is(0));
}
@Test
@ -322,7 +286,7 @@ public class ByteRangeTest
}
@Test
public void testBadRangeTrippleDash()
public void testBadRangeTripleDash()
{
assertBadRangeList(500, "bytes=1---");
}

View File

@ -34,6 +34,7 @@ import java.util.stream.Stream;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.toolchain.test.Hex;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.QuotedStringTokenizer;
@ -254,9 +255,9 @@ public class MultiPartCaptureTest
assertThat("Part[" + expected.name + "]", parts, is(notNullValue()));
MultiPart.Part part = parts.get(0);
String charset = getCharsetFromContentType(part.getHeaders().get(HttpHeader.CONTENT_TYPE), defaultCharset);
Promise.Completable<String> promise = new Promise.Completable<>();
Content.Source.asString(part.getContent(), Charset.forName(charset), promise);
assertThat("Part[" + expected.name + "].contents", promise.get(), containsString(expected.value));
assertTrue(part.getContent().rewind());
String partContent = Content.Source.asString(part.getContent(), Charset.forName(charset));
assertThat("Part[" + expected.name + "].contents", partContent, containsString(expected.value));
}
// Evaluate expected filenames
@ -309,6 +310,7 @@ public class MultiPartCaptureTest
{
// Preserve parts order.
private final Map<String, List<MultiPart.Part>> parts = new LinkedHashMap<>();
private final List<ByteBuffer> partByteBuffers = new ArrayList<>();
private final MultiPartExpectations expectations;
private TestPartsListener(MultiPartExpectations expectations)
@ -317,16 +319,18 @@ public class MultiPartCaptureTest
}
@Override
public void onPart(MultiPart.Part part)
public void onPartContent(Content.Chunk chunk)
{
// Copy the part content, as we need to iterate over it multiple times.
Promise.Completable<List<ByteBuffer>> promise = new Promise.Completable<>();
Content.Source.asByteBuffers(part.getContent(), promise);
promise.thenAccept(byteBuffers ->
{
MultiPart.Part newPart = new MultiPart.ByteBufferPart(part.getName(), part.getFileName(), part.getHeaders(), byteBuffers);
parts.compute(newPart.getName(), (k, v) -> v == null ? new ArrayList<>() : v).add(newPart);
});
partByteBuffers.add(BufferUtil.copy(chunk.getByteBuffer()));
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
MultiPart.Part newPart = new MultiPart.ByteBufferPart(name, fileName, headers, List.copyOf(partByteBuffers));
partByteBuffers.clear();
parts.compute(newPart.getName(), (k, v) -> v == null ? new ArrayList<>() : v).add(newPart);
}
private void assertParts() throws Exception

View File

@ -45,7 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MultiPartsTest
public class MultiPartFormDataTest
{
private static final AtomicInteger testCounter = new AtomicInteger();
@ -76,14 +76,14 @@ public class MultiPartsTest
"Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
"\r\n";
MultiParts multiParts = new MultiParts(boundary);
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(str), true));
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(str), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertInstanceOf(BadMessageException.class, failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("bad last boundary"));
@ -105,14 +105,14 @@ public class MultiPartsTest
eol +
"--" + boundary + "--" + eol;
MultiParts multiParts = new MultiParts(boundary);
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(str), true));
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(str), true));
multiParts.whenComplete((parts, failure) ->
formData.whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -130,14 +130,14 @@ public class MultiPartsTest
String str = eol +
"--" + boundary + "--" + eol;
MultiParts multiParts = new MultiParts(boundary);
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(str), true));
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(str), true));
multiParts.whenComplete((parts, failure) ->
formData.whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -177,14 +177,14 @@ public class MultiPartsTest
----\r
""";
MultiParts multiParts = new MultiParts("");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(str), true));
MultiPartFormData formData = new MultiPartFormData("");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(str), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(4));
MultiPart.Part fileName = parts.getFirst("fileName");
@ -215,10 +215,10 @@ public class MultiPartsTest
@Test
public void testNoBody() throws Exception
{
MultiParts multiParts = new MultiParts("boundary");
multiParts.parse(Content.Chunk.from(ByteBuffer.allocate(0), true));
MultiPartFormData formData = new MultiPartFormData("boundary");
formData.parse(Content.Chunk.from(ByteBuffer.allocate(0), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertNotNull(failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("unexpected EOF"));
@ -229,11 +229,11 @@ public class MultiPartsTest
@Test
public void testBodyWithOnlyCRLF() throws Exception
{
MultiParts multiParts = new MultiParts("boundary");
MultiPartFormData formData = new MultiPartFormData("boundary");
String body = " \n\n\n\r\n\r\n\r\n\r\n";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertNotNull(failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("unexpected EOF"));
@ -263,14 +263,14 @@ public class MultiPartsTest
--AaB03x--\r
""";
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(2));
MultiPart.Part part1 = parts.getFirst("field1");
@ -299,14 +299,14 @@ public class MultiPartsTest
--AaB03x--\r
""";
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(1024);
multiParts.setMaxLength(3072);
multiParts.setMaxMemoryFileSize(50);
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
// The first boundary must be on a new line, so the first "part" is not recognized as such.
assertThat(parts.size(), is(1));
@ -319,8 +319,8 @@ public class MultiPartsTest
@Test
public void testDefaultLimits() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
--AaB03x\r
Content-Disposition: form-data; name="file"; filename="file.txt"\r
@ -329,9 +329,9 @@ public class MultiPartsTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
assertEquals("file", part.getName());
@ -346,9 +346,9 @@ public class MultiPartsTest
@Test
public void testRequestContentTooBig() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxLength(16);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxLength(16);
String body = """
--AaB03x\r
@ -358,9 +358,9 @@ public class MultiPartsTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertNotNull(failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("max length exceeded"));
@ -371,9 +371,9 @@ public class MultiPartsTest
@Test
public void testFileTooBig() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxFileSize(16);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(16);
String body = """
--AaB03x\r
@ -383,9 +383,9 @@ public class MultiPartsTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertNotNull(failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("max file size exceeded"));
@ -396,10 +396,10 @@ public class MultiPartsTest
@Test
public void testTwoFilesOneInMemoryOneOnDisk() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
multiParts.setMaxMemoryFileSize(chunk.length() + 1);
formData.setMaxMemoryFileSize(chunk.length() + 1);
String body = """
--AaB03x\r
@ -414,9 +414,9 @@ public class MultiPartsTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertNotNull(parts);
assertEquals(2, parts.size());
@ -434,10 +434,10 @@ public class MultiPartsTest
@Test
public void testPartWrite() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
multiParts.setMaxMemoryFileSize(chunk.length() + 1);
formData.setMaxMemoryFileSize(chunk.length() + 1);
String body = """
--AaB03x\r
@ -452,9 +452,9 @@ public class MultiPartsTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertNotNull(parts);
assertEquals(2, parts.size());
@ -478,8 +478,8 @@ public class MultiPartsTest
@Test
public void testPathPartDelete() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
--AaB03x\r
@ -489,9 +489,9 @@ public class MultiPartsTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertNotNull(parts);
assertEquals(1, parts.size());
@ -506,9 +506,9 @@ public class MultiPartsTest
@Test
public void testAbort()
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxMemoryFileSize(32);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(32);
String body = """
--AaB03x\r
@ -522,26 +522,26 @@ public class MultiPartsTest
--AaB03x--\r
""";
// Parse only part of the content.
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), false));
assertEquals(1, multiParts.getParts().size());
formData.parse(Content.Chunk.from(UTF_8.encode(body), false));
assertEquals(1, formData.getParts().size());
// Abort MultiParts.
multiParts.completeExceptionally(new IOException());
// Abort MultiPartFormData.
formData.completeExceptionally(new IOException());
// Parse the rest of the content.
multiParts.parse(Content.Chunk.from(UTF_8.encode(terminator), true));
formData.parse(Content.Chunk.from(UTF_8.encode(terminator), true));
// Try to get the parts, it should fail.
assertThrows(ExecutionException.class, () -> multiParts.get(5, TimeUnit.SECONDS));
assertEquals(0, multiParts.getParts().size());
assertThrows(ExecutionException.class, () -> formData.get(5, TimeUnit.SECONDS));
assertEquals(0, formData.getParts().size());
}
@Test
public void testMaxHeaderLength() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setPartHeadersMaxLength(32);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setPartHeadersMaxLength(32);
String body = """
--AaB03x\r
@ -551,9 +551,9 @@ public class MultiPartsTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
multiParts.handle((parts, failure) ->
formData.handle((parts, failure) ->
{
assertNotNull(failure);
assertThat(failure.getMessage(), containsStringIgnoringCase("headers max length exceeded: 32"));
@ -564,9 +564,9 @@ public class MultiPartsTest
@Test
public void testDefaultCharset() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxMemoryFileSize(-1);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
String body1 = """
--AaB03x\r
@ -591,15 +591,15 @@ public class MultiPartsTest
\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body1), false));
multiParts.parse(Content.Chunk.from(isoCedilla, false));
multiParts.parse(Content.Chunk.from(UTF_8.encode(body2), false));
multiParts.parse(Content.Chunk.from(utfCedilla, false));
multiParts.parse(Content.Chunk.from(UTF_8.encode(terminator), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body1), false));
formData.parse(Content.Chunk.from(isoCedilla, false));
formData.parse(Content.Chunk.from(UTF_8.encode(body2), false));
formData.parse(Content.Chunk.from(utfCedilla, false));
formData.parse(Content.Chunk.from(UTF_8.encode(terminator), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
Charset defaultCharset = multiParts.getDefaultCharset();
Charset defaultCharset = formData.getDefaultCharset();
assertEquals(ISO_8859_1, defaultCharset);
MultiPart.Part iso = parts.getFirst("iso");
@ -614,9 +614,9 @@ public class MultiPartsTest
@Test
public void testPartWithBackSlashInFileName() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxMemoryFileSize(-1);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
String contents = """
--AaB03x\r
@ -626,9 +626,9 @@ public class MultiPartsTest
stuffaaa\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(contents), true));
formData.parse(Content.Chunk.from(UTF_8.encode(contents), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -638,9 +638,9 @@ public class MultiPartsTest
@Test
public void testPartWithWindowsFileName() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxMemoryFileSize(-1);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
String contents = """
--AaB03x\r
@ -650,9 +650,9 @@ public class MultiPartsTest
stuffaaa\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(contents), true));
formData.parse(Content.Chunk.from(UTF_8.encode(contents), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -662,9 +662,9 @@ public class MultiPartsTest
@Test
public void testCorrectlyEncodedMSFilename() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setMaxMemoryFileSize(-1);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
String contents = """
--AaB03x\r
@ -674,9 +674,9 @@ public class MultiPartsTest
stuffaaa\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(contents), true));
formData.parse(Content.Chunk.from(UTF_8.encode(contents), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -686,9 +686,9 @@ public class MultiPartsTest
@Test
public void testWriteFilesForPartWithoutFileName() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
multiParts.setUseFilesForPartsWithoutFileName(true);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setUseFilesForPartsWithoutFileName(true);
String body = """
--AaB03x\r
@ -698,9 +698,9 @@ public class MultiPartsTest
sssaaa\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(body), true));
formData.parse(Content.Chunk.from(UTF_8.encode(body), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -713,8 +713,8 @@ public class MultiPartsTest
@Test
public void testPartsWithSameName() throws Exception
{
MultiParts multiParts = new MultiParts("AaB03x");
multiParts.setFilesDirectory(_tmpDir);
MultiPartFormData formData = new MultiPartFormData("AaB03x");
formData.setFilesDirectory(_tmpDir);
String sameNames = """
--AaB03x\r
@ -729,9 +729,9 @@ public class MultiPartsTest
AAAAA\r
--AaB03x--\r
""";
multiParts.parse(Content.Chunk.from(UTF_8.encode(sameNames), true));
formData.parse(Content.Chunk.from(UTF_8.encode(sameNames), true));
MultiParts.Parts parts = multiParts.get(5, TimeUnit.SECONDS);
MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS);
assertEquals(2, parts.size());
@ -741,10 +741,10 @@ public class MultiPartsTest
MultiPart.Part part1 = partsList.get(0);
assertEquals("stuff1.txt", part1.getFileName());
assertEquals("00000", part1.getContentAsString(multiParts.getDefaultCharset()));
assertEquals("00000", part1.getContentAsString(formData.getDefaultCharset()));
MultiPart.Part part2 = partsList.get(1);
assertEquals("stuff2.txt", part2.getFileName());
assertEquals("AAAAA", part2.getContentAsString(multiParts.getDefaultCharset()));
assertEquals("AAAAA", part2.getContentAsString(formData.getDefaultCharset()));
}
}

View File

@ -673,13 +673,21 @@ public class MultiPartTest
private static class TestPartsListener extends MultiPart.AbstractPartsListener
{
private final List<MultiPart.Part> parts = new ArrayList<>();
private final List<Content.Chunk> partContent = new ArrayList<>();
private boolean complete;
private Throwable failure;
@Override
public void onPart(MultiPart.Part part)
public void onPartContent(Content.Chunk chunk)
{
parts.add(part);
partContent.add(chunk);
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partContent)));
partContent.clear();
}
@Override

View File

@ -19,7 +19,6 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Flow;
import java.util.function.BiPredicate;
@ -32,7 +31,6 @@ import org.eclipse.jetty.io.content.ContentSourcePublisher;
import org.eclipse.jetty.io.internal.ByteBufferChunk;
import org.eclipse.jetty.io.internal.ContentCopier;
import org.eclipse.jetty.io.internal.ContentSourceByteBuffer;
import org.eclipse.jetty.io.internal.ContentSourceByteBuffers;
import org.eclipse.jetty.io.internal.ContentSourceConsumer;
import org.eclipse.jetty.io.internal.ContentSourceString;
import org.eclipse.jetty.util.Blocker;
@ -158,19 +156,6 @@ public class Content
}
}
/**
* <p>Reads, non-blocking, the whole content source, copying each chunk's
* {@code ByteBuffer} into a new {@code ByteBuffer} that is added to
* a list returned as result.</p>
*
* @param source the source to read
* @param promise the promise to notify when the whole content has been read into a list of ByteBuffers
*/
static void asByteBuffers(Source source, Promise<List<ByteBuffer>> promise)
{
new ContentSourceByteBuffers(source, promise).run();
}
/**
* <p>Reads, non-blocking, the whole content source into a {@link String}, converting the bytes
* using the given {@link Charset}.</p>

View File

@ -13,8 +13,10 @@
package org.eclipse.jetty.io.content;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.thread.AutoLock;
@ -113,11 +115,18 @@ public class ChunksContentSource implements Content.Source
@Override
public void fail(Throwable failure)
{
List<Content.Chunk> toFail = List.of();
try (AutoLock ignored = lock.lock())
{
if (terminated != null)
return;
terminated = Content.Chunk.from(failure);
if (iterator != null)
{
toFail = new ArrayList<>();
iterator.forEachRemaining(toFail::add);
}
}
toFail.forEach(Content.Chunk::release);
}
}

View File

@ -14,8 +14,9 @@
package org.eclipse.jetty.io.content;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
@ -43,30 +44,37 @@ public class PathContentSource implements Content.Source
private final RetainableByteBufferPool byteBufferPool;
private int bufferSize = 4096;
private boolean useDirectByteBuffers = true;
private ReadableByteChannel channel;
private SeekableByteChannel channel;
private long totalRead;
private Runnable demandCallback;
private Content.Chunk.Error errorChunk;
public PathContentSource(Path path) throws IOException
public PathContentSource(Path path)
{
this(path, (ByteBufferPool)null);
}
public PathContentSource(Path path, ByteBufferPool byteBufferPool) throws IOException
public PathContentSource(Path path, ByteBufferPool byteBufferPool)
{
this(path, (byteBufferPool == null ? ByteBufferPool.NOOP : byteBufferPool).asRetainableByteBufferPool());
}
public PathContentSource(Path path, RetainableByteBufferPool byteBufferPool) throws IOException
public PathContentSource(Path path, RetainableByteBufferPool byteBufferPool)
{
if (!Files.isRegularFile(path))
throw new NoSuchFileException(path.toString());
if (!Files.isReadable(path))
throw new AccessDeniedException(path.toString());
this.path = path;
this.length = Files.size(path);
this.byteBufferPool = byteBufferPool == null ? ByteBufferPool.NOOP.asRetainableByteBufferPool() : byteBufferPool;
try
{
if (!Files.isRegularFile(path))
throw new NoSuchFileException(path.toString());
if (!Files.isReadable(path))
throw new AccessDeniedException(path.toString());
this.path = path;
this.length = Files.size(path);
this.byteBufferPool = byteBufferPool == null ? ByteBufferPool.NOOP.asRetainableByteBufferPool() : byteBufferPool;
}
catch (IOException x)
{
throw new UncheckedIOException(x);
}
}
public Path getPath()
@ -103,7 +111,7 @@ public class PathContentSource implements Content.Source
@Override
public Content.Chunk read()
{
ReadableByteChannel channel;
SeekableByteChannel channel;
try (AutoLock ignored = lock.lock())
{
if (errorChunk != null)
@ -113,7 +121,7 @@ public class PathContentSource implements Content.Source
{
try
{
this.channel = Files.newByteChannel(path, StandardOpenOption.READ);
this.channel = open();
}
catch (Throwable x)
{
@ -126,14 +134,14 @@ public class PathContentSource implements Content.Source
if (!channel.isOpen())
return Content.Chunk.EOF;
RetainableByteBuffer retainableBuffer = byteBufferPool.acquire(getBufferSize(), isUseDirectByteBuffers());
ByteBuffer byteBuffer = retainableBuffer.getBuffer();
RetainableByteBuffer retainableByteBuffer = byteBufferPool.acquire(getBufferSize(), isUseDirectByteBuffers());
ByteBuffer byteBuffer = retainableByteBuffer.getBuffer();
int read;
try
{
BufferUtil.clearToFill(byteBuffer);
read = channel.read(byteBuffer);
read = read(channel, byteBuffer);
BufferUtil.flipToFlush(byteBuffer, 0);
}
catch (Throwable x)
@ -144,11 +152,26 @@ public class PathContentSource implements Content.Source
if (read > 0)
totalRead += read;
boolean last = totalRead == getLength();
boolean last = isReadComplete(totalRead);
if (last)
IO.close(channel);
return Content.Chunk.from(byteBuffer, last, retainableBuffer);
return Content.Chunk.from(byteBuffer, last, retainableByteBuffer);
}
protected SeekableByteChannel open() throws IOException
{
return Files.newByteChannel(path, StandardOpenOption.READ);
}
protected int read(SeekableByteChannel channel, ByteBuffer byteBuffer) throws IOException
{
return channel.read(byteBuffer);
}
protected boolean isReadComplete(long read)
{
return read == getLength();
}
@Override

View File

@ -1,67 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.io.internal;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Promise;
public class ContentSourceByteBuffers implements Runnable
{
private final List<ByteBuffer> accumulator = new ArrayList<>();
private final Content.Source source;
private final Promise<List<ByteBuffer>> promise;
public ContentSourceByteBuffers(Content.Source source, Promise<List<ByteBuffer>> promise)
{
this.source = source;
this.promise = promise;
}
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = source.read();
if (chunk == null)
{
source.demand(this);
return;
}
if (chunk instanceof Content.Chunk.Error error)
{
promise.failed(error.getCause());
return;
}
ByteBuffer byteBuffer = chunk.getByteBuffer();
if (byteBuffer.hasRemaining())
accumulator.add(BufferUtil.copy(byteBuffer));
chunk.release();
if (chunk.isLast())
{
promise.succeeded(accumulator);
return;
}
}
}
}

View File

@ -35,6 +35,8 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartByteRanges;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.http.QuotedQualityCSV;
@ -151,13 +153,10 @@ public class ResourceService
String pathInContext = request.getPathInContext();
// Is this a Range request?
Enumeration<String> reqRanges = request.getHeaders().getValues(HttpHeader.RANGE.asString());
if (!hasDefinedRange(reqRanges))
reqRanges = null;
List<String> reqRanges = request.getHeaders().getValuesList(HttpHeader.RANGE.asString());
boolean endsWithSlash = pathInContext.endsWith(URIUtil.SLASH);
boolean checkPrecompressedVariants = _precompressedFormats.size() > 0 && !endsWithSlash && reqRanges == null;
boolean checkPrecompressedVariants = _precompressedFormats.size() > 0 && !endsWithSlash && reqRanges.isEmpty();
try
{
@ -450,13 +449,14 @@ public class ResourceService
* as determined by {@link ResourceService#processWelcome(Request, Response)}
*
* <p>
* For {@link WelcomeActionType#REDIRECT} this is the resulting `Location` response header.
* For {@link WelcomeActionType#SERVE} this is the resulting path to for welcome serve, note that
* this is just a path, and can point to a real file, or a dynamic handler for
* welcome processing (such as Jetty core Handler, or EE Servlet), it's up
* to the implementation of {@link ResourceService#welcome(Request, Response, Callback)}
* to handle the various action types.
* For {@link WelcomeActionType#REDIRECT} this is the resulting `Location` response header.
* For {@link WelcomeActionType#SERVE} this is the resulting path to for welcome serve, note that
* this is just a path, and can point to a real file, or a dynamic handler for
* welcome processing (such as Jetty core Handler, or EE Servlet), it's up
* to the implementation of {@link ResourceService#welcome(Request, Response, Callback)}
* to handle the various action types.
* </p>
*
* @param type the type of action
* @param target The target URI path of the action.
*/
@ -486,7 +486,7 @@ public class ResourceService
{
// TODO : check conditional headers.
HttpContent c = _contentFactory.getContent(welcomeAction.target);
sendData(request, response, callback, c, null);
sendData(request, response, callback, c, List.of());
}
}
}
@ -540,133 +540,57 @@ public class ResourceService
response.write(true, ByteBuffer.wrap(data), callback);
}
private boolean sendData(Request request, Response response, Callback callback, HttpContent content, Enumeration<String> reqRanges) throws IOException
private void sendData(Request request, Response response, Callback callback, HttpContent content, List<String> reqRanges)
{
long contentLength = content.getContentLengthValue();
if (LOG.isDebugEnabled())
LOG.debug(String.format("sendData content=%s", content));
if (reqRanges == null || !reqRanges.hasMoreElements())
if (reqRanges.isEmpty())
{
// if there were no ranges, send entire entity
// write the headers
// If there are no ranges, send the entire content.
if (contentLength >= 0)
putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH);
else
putHeaders(response, content, NO_CONTENT_LENGTH);
// write the content
writeHttpContent(request, response, callback, content);
return;
}
else
// Parse the satisfiable ranges.
List<ByteRange> ranges = ByteRange.parse(reqRanges, contentLength);
// If there are no satisfiable ranges, send a 416 response.
if (ranges.isEmpty())
{
throw new UnsupportedOperationException("TODO ranges not yet supported");
// TODO rewrite with ByteChannel only which should simplify HttpContentRangeWriter as HttpContent's Path always provides a SeekableByteChannel
// but MultiPartOutputStream also needs to be rewritten.
/*
// Parse the satisfiable ranges
List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, contentLength);
// if there are no satisfiable ranges, send 416 response
if (ranges == null || ranges.size() == 0)
{
putHeaders(response, content, USE_KNOWN_CONTENT_LENGTH);
response.getHeaders().put(HttpHeader.CONTENT_RANGE,
InclusiveByteRange.to416HeaderRangeString(contentLength));
writeHttpError(request, response, callback, HttpStatus.RANGE_NOT_SATISFIABLE_416);
return true;
}
// if there is only a single valid range (must be satisfiable
// since were here now), send that range with a 216 response
if (ranges.size() == 1)
{
InclusiveByteRange singleSatisfiableRange = ranges.iterator().next();
long singleLength = singleSatisfiableRange.getSize();
putHeaders(response, content, singleLength);
response.setStatus(206);
if (!response.getHeaders().contains(HttpHeader.DATE.asString()))
response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis());
response.getHeaders().put(HttpHeader.CONTENT_RANGE,
singleSatisfiableRange.toHeaderRangeString(contentLength));
writeHttpPartialContent(request, response, callback, content, singleSatisfiableRange);
return true;
}
// multiple non-overlapping valid ranges cause a multipart
// 216 response which does not require an overall
// content-length header
//
putHeaders(response, content, NO_CONTENT_LENGTH);
String mimetype = content.getContentTypeValue();
if (mimetype == null)
LOG.warn("Unknown mimetype for {}", request.getHttpURI());
response.setStatus(206);
if (!response.getHeaders().contains(HttpHeader.DATE.asString()))
response.getHeaders().addDateField(HttpHeader.DATE.asString(), System.currentTimeMillis());
// If the request has a "Request-Range" header then we need to
// send an old style multipart/x-byteranges Content-Type. This
// keeps Netscape and acrobat happy. This is what Apache does.
String ctp;
if (request.getHeaders().get(HttpHeader.REQUEST_RANGE.asString()) != null)
ctp = "multipart/x-byteranges; boundary=";
else
ctp = "multipart/byteranges; boundary=";
MultiPartOutputStream multi = new MultiPartOutputStream(out);
response.setContentType(ctp + multi.getBoundary());
// calculate the content-length
int length = 0;
String[] header = new String[ranges.size()];
int i = 0;
final int CRLF = "\r\n".length();
final int DASHDASH = "--".length();
final int BOUNDARY = multi.getBoundary().length();
final int FIELD_SEP = ": ".length();
for (InclusiveByteRange ibr : ranges)
{
header[i] = ibr.toHeaderRangeString(contentLength);
if (i > 0) // in-part
length += CRLF;
length += DASHDASH + BOUNDARY + CRLF;
if (mimetype != null)
length += HttpHeader.CONTENT_TYPE.asString().length() + FIELD_SEP + mimetype.length() + CRLF;
length += HttpHeader.CONTENT_RANGE.asString().length() + FIELD_SEP + header[i].length() + CRLF;
length += CRLF;
length += ibr.getSize();
i++;
}
length += CRLF + DASHDASH + BOUNDARY + DASHDASH + CRLF;
response.setContentLength(length);
try (RangeWriter rangeWriter = HttpContentRangeWriter.newRangeWriter(content))
{
i = 0;
for (InclusiveByteRange ibr : ranges)
{
multi.startPart(mimetype, new String[]{HttpHeader.CONTENT_RANGE + ": " + header[i]});
rangeWriter.writeTo(multi, ibr.getFirst(), ibr.getSize());
i++;
}
}
multi.close();
*/
response.getHeaders().put(HttpHeader.CONTENT_RANGE, ByteRange.toNonSatisfiableHeaderValue(contentLength));
Response.writeError(request, response, callback, HttpStatus.RANGE_NOT_SATISFIABLE_416);
return;
}
return true;
}
protected void writeHttpPartialContent(Request request, Response response, Callback callback, HttpContent content, ByteRange singleSatisfiableRange)
{
// TODO: implement this
}
// If there is only a single valid range, send that range with a 206 response.
if (ranges.size() == 1)
{
ByteRange range = ranges.get(0);
putHeaders(response, content, range.getLength());
response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
response.getHeaders().put(HttpHeader.CONTENT_RANGE, range.toHeaderValue(contentLength));
Content.copy(new MultiPartByteRanges.PathContentSource(content.getResource().getPath(), range), response, callback);
return;
}
protected void writeHttpError(Request request, Response response, Callback callback, int statusCode)
{
Response.writeError(request, response, callback, statusCode);
// There are multiple non-overlapping ranges, send a multipart/byteranges 206 response.
putHeaders(response, content, NO_CONTENT_LENGTH);
response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
String contentType = "multipart/byteranges; boundary=";
String boundary = MultiPart.generateBoundary(null, 24);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType + boundary);
MultiPartByteRanges.ContentSource byteRanges = new MultiPartByteRanges.ContentSource(boundary);
ranges.forEach(range -> byteRanges.addPart(new MultiPartByteRanges.Part(content.getContentTypeValue(), content.getResource().getPath(), range)));
byteRanges.close();
Content.copy(byteRanges, response, callback);
}
protected void writeHttpContent(Request request, Response response, Callback callback, HttpContent content)
@ -738,11 +662,6 @@ public class ResourceService
response.getHeaders().put(_cacheControl);
}
private boolean hasDefinedRange(Enumeration<String> reqRanges)
{
return (reqRanges != null && reqRanges.hasMoreElements());
}
/**
* @return If true, range requests and responses are supported
*/
@ -874,7 +793,7 @@ public class ResourceService
*
* @param request the request to use to determine the matching welcome target from.
* @return The URI path of the matching welcome target in context or null
* (null means no welcome target was found)
* (null means no welcome target was found)
*/
String getWelcomeTarget(Request request) throws IOException;
}

View File

@ -21,7 +21,8 @@ import java.util.function.BiConsumer;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiParts;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Handler;
@ -146,7 +147,7 @@ public abstract class DelayedHandler extends Handler.Wrapper
}
}
public static class UntilMultiPart extends DelayedHandler
public static class UntilMultiPartFormData extends DelayedHandler
{
@Override
protected Request.Processor delayed(Request request, Request.Processor processor)
@ -161,22 +162,27 @@ public abstract class DelayedHandler extends Handler.Wrapper
String contentTypeValue = HttpField.valueParameters(contentType, null);
if (!MimeTypes.Type.MULTIPART_FORM_DATA.is(contentTypeValue))
return processor;
String boundary = MultiPart.extractBoundary(contentType);
if (boundary == null)
return processor;
return new Processor(request, processor);
return new Processor(request, processor, boundary);
}
private static class Processor implements Request.Processor, Runnable, BiConsumer<MultiParts.Parts, Throwable>
private static class Processor implements Request.Processor, Runnable, BiConsumer<MultiPartFormData.Parts, Throwable>
{
private final Request _request;
private final Request.Processor _processor;
private final String _boundary;
private Response _response;
private Callback _callback;
private MultiParts _multiParts;
private MultiPartFormData _formData;
private Processor(Request request, Request.Processor processor)
private Processor(Request request, Request.Processor processor, String boundary)
{
_request = request;
_processor = processor;
_boundary = boundary;
}
@Override
@ -186,17 +192,16 @@ public abstract class DelayedHandler extends Handler.Wrapper
_callback = callback;
String contentType = _request.getHeaders().get(HttpHeader.CONTENT_TYPE);
String boundary = MultiParts.extractBoundary(contentType);
_multiParts = new MultiParts(boundary);
// TODO: configure _multiParts.
_request.setAttribute(MultiParts.class.getName(), _multiParts);
_formData = new MultiPartFormData(_boundary);
// TODO: configure _formData.
_request.setAttribute(MultiPartFormData.class.getName(), _formData);
run();
if (_multiParts.isDone())
if (_formData.isDone())
_processor.process(_request, response, callback);
else
_multiParts.whenComplete(this);
_formData.whenComplete(this);
}
@Override
@ -212,10 +217,10 @@ public abstract class DelayedHandler extends Handler.Wrapper
}
if (chunk instanceof Content.Chunk.Error error)
{
_multiParts.completeExceptionally(error.getCause());
_formData.completeExceptionally(error.getCause());
return;
}
_multiParts.parse(chunk);
_formData.parse(chunk);
chunk.release();
if (chunk.isLast())
return;
@ -223,7 +228,7 @@ public abstract class DelayedHandler extends Handler.Wrapper
}
@Override
public void accept(MultiParts.Parts parts, Throwable throwable)
public void accept(MultiPartFormData.Parts parts, Throwable throwable)
{
try
{

View File

@ -1,59 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.resource;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
/**
* ByteBuffer based RangeWriter
*/
public class ByteBufferRangeWriter implements RangeWriter
{
private final ByteBuffer buffer;
private boolean closed = false;
public ByteBufferRangeWriter(ByteBuffer buffer)
{
this.buffer = buffer.asReadOnlyBuffer();
}
@Override
public void close() throws IOException
{
closed = true;
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
if (skipTo > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Unsupported skipTo " + skipTo + " > " + Integer.MAX_VALUE);
}
if (length > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Unsupported length " + skipTo + " > " + Integer.MAX_VALUE);
}
ByteBuffer src = buffer.slice();
src.position((int)skipTo);
src.limit(Math.addExact((int)skipTo, (int)length));
BufferUtil.writeTo(src, outputStream);
}
}

View File

@ -1,120 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.jetty.util.IO;
/**
* Default Range Writer for InputStream
*/
public class InputStreamRangeWriter implements RangeWriter
{
public static final int NO_PROGRESS_LIMIT = 3;
public interface InputStreamSupplier
{
InputStream newInputStream() throws IOException;
}
private final InputStreamSupplier inputStreamSupplier;
private boolean closed = false;
private InputStream inputStream;
private long pos;
/**
* Create InputStreamRangeWriter
*
* @param inputStreamSupplier Supplier of the InputStream. If the stream needs to be regenerated, such as the next
* requested range being before the current position, then the current InputStream is closed and a new one obtained
* from this supplier.
*/
public InputStreamRangeWriter(InputStreamSupplier inputStreamSupplier)
{
this.inputStreamSupplier = inputStreamSupplier;
}
@Override
public void close() throws IOException
{
closed = true;
if (inputStream != null)
{
inputStream.close();
}
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
if (closed)
{
throw new IOException("RangeWriter is closed");
}
if (inputStream == null)
{
inputStream = inputStreamSupplier.newInputStream();
pos = 0;
}
if (skipTo < pos)
{
inputStream.close();
inputStream = inputStreamSupplier.newInputStream();
pos = 0;
}
if (pos < skipTo)
{
long skipSoFar = pos;
long actualSkipped;
int noProgressLoopLimit = NO_PROGRESS_LIMIT;
// loop till we reach desired point, break out on lack of progress.
while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
{
actualSkipped = inputStream.skip(skipTo - skipSoFar);
if (actualSkipped == 0)
{
noProgressLoopLimit--;
}
else if (actualSkipped > 0)
{
skipSoFar += actualSkipped;
noProgressLoopLimit = NO_PROGRESS_LIMIT;
}
else
{
// negative values means the stream was closed or reached EOF
// either way, we've hit a state where we can no longer
// fulfill the requested range write.
throw new IOException("EOF reached before InputStream skip destination");
}
}
if (noProgressLoopLimit <= 0)
{
throw new IOException("No progress made to reach InputStream skip position " + (skipTo - pos));
}
pos = skipTo;
}
IO.copy(inputStream, outputStream, length);
pos += length;
}
}

View File

@ -1,33 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.resource;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
/**
* Interface for writing sections (ranges) of a single resource (SeekableByteChannel, Resource, etc) to an outputStream.
*/
public interface RangeWriter extends Closeable
{
/**
* Write the specific range (start, size) to the outputStream.
*
* @param outputStream the stream to write to
* @param skipTo the offset / skip-to / seek-to / position in the resource to start the write from
* @param length the size of the section to write
*/
void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException;
}

View File

@ -1,161 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.resource;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
public class SeekableByteChannelRangeWriter implements RangeWriter
{
public static final int NO_PROGRESS_LIMIT = 3;
public interface ChannelSupplier
{
SeekableByteChannel newSeekableByteChannel() throws IOException;
}
private final ChannelSupplier channelSupplier;
private final int bufSize;
private final ByteBuffer buffer;
private SeekableByteChannel channel;
private long pos;
private boolean defaultSeekMode = true;
public SeekableByteChannelRangeWriter(SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
{
this(null, channelSupplier);
}
public SeekableByteChannelRangeWriter(SeekableByteChannel initialChannel, SeekableByteChannelRangeWriter.ChannelSupplier channelSupplier)
{
this.channel = initialChannel;
this.channelSupplier = channelSupplier;
this.bufSize = IO.bufferSize;
this.buffer = BufferUtil.allocate(this.bufSize);
}
@Override
public void close() throws IOException
{
if (this.channel != null)
{
this.channel.close();
}
}
@Override
public void writeTo(OutputStream outputStream, long skipTo, long length) throws IOException
{
skipTo(skipTo);
// copy from channel to output stream
long readTotal = 0;
while (readTotal < length)
{
BufferUtil.clearToFill(buffer);
int size = (int)Math.min(bufSize, length - readTotal);
buffer.limit(size);
int readLen = channel.read(buffer);
BufferUtil.flipToFlush(buffer, 0);
BufferUtil.writeTo(buffer, outputStream);
readTotal += readLen;
pos += readLen;
}
}
private void skipTo(long skipTo) throws IOException
{
if (channel == null)
{
channel = channelSupplier.newSeekableByteChannel();
pos = 0;
}
if (defaultSeekMode)
{
try
{
if (channel.position() != skipTo)
{
channel.position(skipTo);
pos = skipTo;
return;
}
}
catch (UnsupportedOperationException e)
{
defaultSeekMode = false;
fallbackSkipTo(skipTo);
}
}
else
{
// Fallback mode
fallbackSkipTo(skipTo);
}
}
private void fallbackSkipTo(long skipTo) throws IOException
{
if (skipTo < pos)
{
channel.close();
channel = channelSupplier.newSeekableByteChannel();
pos = 0;
}
if (pos < skipTo)
{
long skipSoFar = pos;
long actualSkipped;
int noProgressLoopLimit = NO_PROGRESS_LIMIT;
// loop till we reach desired point, break out on lack of progress.
while (noProgressLoopLimit > 0 && skipSoFar < skipTo)
{
BufferUtil.clearToFill(buffer);
int len = (int)Math.min(bufSize, (skipTo - skipSoFar));
buffer.limit(len);
actualSkipped = channel.read(buffer);
if (actualSkipped == 0)
{
noProgressLoopLimit--;
}
else if (actualSkipped > 0)
{
skipSoFar += actualSkipped;
noProgressLoopLimit = NO_PROGRESS_LIMIT;
}
else
{
// negative values means the stream was closed or reached EOF
// either way, we've hit a state where we can no longer
// fulfill the requested range write.
throw new IOException("EOF reached before SeekableByteChannel skip destination");
}
}
if (noProgressLoopLimit <= 0)
{
throw new IOException("No progress made to reach SeekableByteChannel skip position " + (skipTo - pos));
}
pos = skipTo;
}
}
}

View File

@ -0,0 +1,128 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.eclipse.jetty.http.ByteRange;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartByteRanges;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MultiPartByteRangesTest
{
private Server server;
private ServerConnector connector;
private void start(Handler handler) throws Exception
{
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
connector = new ServerConnector(server, 1, 1);
server.addConnector(connector);
server.setHandler(handler);
server.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(server);
}
@Test
public void testByteRanges() throws Exception
{
String resourceChars = "0123456789ABCDEF";
Path resourceDir = MavenTestingUtils.getTargetTestingPath(getClass().getSimpleName());
FS.ensureEmpty(resourceDir);
Path resourcePath = resourceDir.resolve("range.txt");
Files.writeString(resourcePath, resourceChars);
start(new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback)
{
assertTrue(request.getHeaders().contains(HttpHeader.ACCEPT_RANGES));
assertTrue(request.getHeaders().contains(HttpHeader.RANGE));
List<ByteRange> ranges = ByteRange.parse(request.getHeaders().getValuesList(HttpHeader.RANGE), resourceChars.length());
String boundary = "boundary";
try (MultiPartByteRanges.ContentSource content = new MultiPartByteRanges.ContentSource(boundary))
{
ranges.forEach(range -> content.addPart(new MultiPartByteRanges.Part("text/plain", resourcePath, range)));
content.close();
response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/byteranges; boundary=" + QuotedStringTokenizer.quote(boundary));
Content.copy(content, response, callback);
}
}
});
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod(HttpMethod.GET.asString());
request.put(HttpHeader.HOST, "localhost");
request.put(HttpHeader.ACCEPT_RANGES, "bytes");
request.put(HttpHeader.RANGE, "bytes=1-2,4-6,-4");
socket.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
assertNotNull(response);
assertEquals(HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
String contentType = response.get(HttpHeader.CONTENT_TYPE);
assertNotNull(contentType);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartByteRanges.Parts parts = byteRanges.join();
assertEquals(3, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("12", Content.Source.asString(part1.getContent()));
MultiPart.Part part2 = parts.get(1);
assertEquals("456", Content.Source.asString(part2.getContent()));
MultiPart.Part part3 = parts.get(2);
assertEquals("CDEF", Content.Source.asString(part3.getContent()));
}
}
}

View File

@ -26,7 +26,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiParts;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.server.Handler;
@ -46,7 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MultiPartHandlerTest
public class MultiPartFormDataHandlerTest
{
private Server server;
private ServerConnector connector;
@ -75,8 +75,8 @@ public class MultiPartHandlerTest
@Override
public void process(Request request, Response response, Callback callback)
{
String boundary = MultiParts.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiParts(boundary).parse(request)
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
@ -115,9 +115,9 @@ public class MultiPartHandlerTest
}
@Test
public void testDelayedUntilMultiPart() throws Exception
public void testDelayedUntilFormData() throws Exception
{
DelayedHandler.UntilMultiPart delayedHandler = new DelayedHandler.UntilMultiPart();
DelayedHandler.UntilMultiPartFormData delayedHandler = new DelayedHandler.UntilMultiPartFormData();
CountDownLatch processLatch = new CountDownLatch(1);
delayedHandler.setHandler(new Handler.Processor()
{
@ -125,9 +125,9 @@ public class MultiPartHandlerTest
public void process(Request request, Response response, Callback callback) throws Exception
{
processLatch.countDown();
MultiParts multiParts = (MultiParts)request.getAttribute(MultiParts.class.getName());
assertNotNull(multiParts);
MultiPart.Part part = multiParts.get().get(0);
MultiPartFormData formData = (MultiPartFormData)request.getAttribute(MultiPartFormData.class.getName());
assertNotNull(formData);
MultiPart.Part part = formData.get().get(0);
Content.copy(part.getContent(), response, callback);
}
});
@ -187,15 +187,17 @@ public class MultiPartHandlerTest
@Override
public void process(Request request, Response response, Callback callback)
{
String boundary = MultiParts.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiParts(boundary).parse(request)
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getBoundary()));
MultiPart.ContentSource source = parts.toContentSource();
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getBoundary());
source.setPartHeadersMaxLength(1024);
parts.forEach(source::addPart);
source.close();
Content.copy(source, response, callback);
}
else
@ -251,7 +253,7 @@ public class MultiPartHandlerTest
String boundary = "A1B2C3";
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(boundary));
MultiPart.ContentSource source = new MultiPart.ContentSource(boundary);
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(boundary);
HttpFields.Mutable headers1 = HttpFields.build().put(HttpHeader.CONTENT_TYPE, "text/plain");
assertTrue(source.addPart(new MultiPart.ByteBufferPart("part1", null, headers1, UTF_8.encode("hello"))));
@ -297,13 +299,13 @@ public class MultiPartHandlerTest
String value = response.get(HttpHeader.CONTENT_TYPE);
String contentType = HttpField.valueParameters(value, null);
assertEquals("multipart/form-data", contentType);
String boundary = MultiParts.extractBoundary(value);
String boundary = MultiPart.extractBoundary(value);
assertNotNull(boundary);
MultiParts multiParts = new MultiParts(boundary);
multiParts.setFilesDirectory(tempDir);
multiParts.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiParts.Parts parts = multiParts.join();
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setFilesDirectory(tempDir);
formData.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartFormData.Parts parts = formData.join();
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);

View File

@ -0,0 +1,190 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartByteRanges;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class ResourceHandlerByteRangesTest
{
private final String rangeChars = "0123456789abcdefghijklmnopqrstuvwxyz";
private Server server;
private ServerConnector connector;
@BeforeEach
public void start() throws Exception
{
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
server = new Server();
connector = new ServerConnector(server, 1, 1);
server.addConnector(connector);
Path dir = MavenTestingUtils.getTargetTestingPath(ResourceHandlerByteRangesTest.class.getSimpleName());
FS.ensureEmpty(dir);
Path rangeFile = dir.resolve("range.txt");
Files.writeString(rangeFile, rangeChars);
ResourceHandler handler = new ResourceHandler();
handler.setBaseResource(ResourceFactory.root().newResource(dir));
server.setHandler(handler);
server.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(server);
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
@ParameterizedTest
@ValueSource(strings = {"bad-unit=0-0", "bytes=not-integer", "bytes=0", "bytes=-", "bytes=2-1", "bytes=100-200"})
public void testBadRange(String rangeValue) throws Exception
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod(HttpMethod.GET.asString());
request.setURI("/range.txt");
request.put(HttpHeader.HOST, "localhost");
request.put(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES);
request.put(HttpHeader.RANGE, rangeValue);
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
socket.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
assertNotNull(response);
assertEquals(HttpStatus.RANGE_NOT_SATISFIABLE_416, response.getStatus());
}
}
@Test
public void testNoRange() throws Exception
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod(HttpMethod.GET.asString());
request.setURI("/range.txt");
request.put(HttpHeader.HOST, "localhost");
request.put(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES);
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
socket.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals(rangeChars, response.getContent());
}
}
@Test
public void testOneRange() throws Exception
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod(HttpMethod.GET.asString());
request.setURI("/range.txt");
request.put(HttpHeader.HOST, "localhost");
request.put(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES);
request.put(HttpHeader.RANGE, "bytes=2-4");
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
socket.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
assertNotNull(response);
assertEquals(HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
assertEquals("text/plain", response.get(HttpHeader.CONTENT_TYPE));
assertEquals("bytes 2-4/" + rangeChars.length(), response.get(HttpHeader.CONTENT_RANGE));
assertEquals("234", response.getContent());
}
}
@Test
public void testTwoRanges() throws Exception
{
testTwoRanges(HttpHeader.RANGE, "multipart/byteranges");
}
@Test
@Disabled
public void testTwoRangesObsolete() throws Exception
{
testTwoRanges(HttpHeader.REQUEST_RANGE, "multipart/x-byteranges");
}
private void testTwoRanges(HttpHeader requestRangeHeader, String responseContentType) throws Exception
{
HttpTester.Request request = HttpTester.newRequest();
request.setMethod(HttpMethod.GET.asString());
request.setURI("/range.txt");
request.put(HttpHeader.HOST, "localhost");
request.put(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES);
request.put(requestRangeHeader, "bytes=2-4,-3");
try (SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
socket.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(socket));
assertNotNull(response);
assertEquals(HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
String contentType = response.get(HttpHeader.CONTENT_TYPE);
assertThat(contentType, startsWith(responseContentType));
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges.Parts parts = new MultiPartByteRanges(boundary)
.parse(new ByteBufferContentSource(response.getContentByteBuffer()))
.join();
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("text/plain", part1.getHeaders().get(HttpHeader.CONTENT_TYPE));
assertEquals("234", Content.Source.asString(part1.getContent()));
MultiPart.Part part2 = parts.get(1);
assertEquals("text/plain", part2.getHeaders().get(HttpHeader.CONTENT_TYPE));
assertEquals("xyz", Content.Source.asString(part2.getContent()));
}
}
}

View File

@ -1,111 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
@Disabled("Unfixed range bug - Issue #107") // TODO long disabled!?!?!?
public class ResourceHandlerRangeTest
{
private static Server server;
private static URI serverUri;
@BeforeAll
public static void startServer() throws Exception
{
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(0);
server.addConnector(connector);
ContextHandlerCollection contexts = new ContextHandlerCollection();
File dir = MavenTestingUtils.getTargetTestingDir(ResourceHandlerRangeTest.class.getSimpleName());
FS.ensureEmpty(dir);
File rangeFile = new File(dir, "range.txt");
try (FileWriter writer = new FileWriter(rangeFile))
{
writer.append("0123456789");
writer.flush();
}
ContextHandler contextHandler = new ContextHandler();
ResourceHandler contentResourceHandler = new ResourceHandler();
contextHandler.setBaseResource(ResourceFactory.root().newResource(dir.getAbsolutePath()));
contextHandler.setHandler(contentResourceHandler);
contextHandler.setContextPath("/");
contexts.addHandler(contextHandler);
server.setHandler(contexts);
server.start();
String host = connector.getHost();
if (host == null)
{
host = "localhost";
}
int port = connector.getLocalPort();
serverUri = new URI(String.format("http://%s:%d/", host, port));
}
@AfterAll
public static void stopServer() throws Exception
{
server.stop();
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
@Test
public void testGetRange() throws Exception
{
URI uri = serverUri.resolve("range.txt");
HttpURLConnection uconn = (HttpURLConnection)uri.toURL().openConnection();
uconn.setRequestMethod("GET");
uconn.addRequestProperty("Range", "bytes=" + 5 + "-");
int contentLength = Integer.parseInt(uconn.getHeaderField("Content-Length"));
String response;
try (InputStream is = uconn.getInputStream())
{
response = IO.toString(is);
}
assertThat("Content Length", contentLength, is(5));
assertThat("Response Content", response, is("56789"));
}
}

View File

@ -1,190 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.resource;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
@Disabled // TODO should not be a writer
public class RangeWriterTest
{
public static final String DATA = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWZYZ!@#$%^&*()_+/.,[]";
private static ResourceFactory.Closeable zipfs;
@BeforeEach
public void beforeEach()
{
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
@AfterEach
public void afterEach()
{
IO.close(zipfs);
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
public static Path initDataFile() throws IOException
{
Path testDir = MavenTestingUtils.getTargetTestingPath(RangeWriterTest.class.getSimpleName());
FS.ensureEmpty(testDir);
Path dataFile = testDir.resolve("data.dat");
try (BufferedWriter writer = Files.newBufferedWriter(dataFile, UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))
{
writer.write(DATA);
writer.flush();
}
return dataFile;
}
private static Path initZipFsDataFile() throws IOException
{
Path exampleJar = MavenTestingUtils.getTestResourcePathFile("example.jar");
// close prior one (if it exists)
IO.close(zipfs);
zipfs = ResourceFactory.closeable();
Path rootPath = zipfs.newJarFileResource(exampleJar.toUri()).getPath();
return rootPath.resolve("data.dat");
}
public static Stream<Arguments> impls() throws IOException
{
Resource realFileSystemResource = zipfs.newResource(initDataFile());
Resource nonDefaultFileSystemResource = zipfs.newResource(initZipFsDataFile());
return Stream.of(
Arguments.of("Traditional / Direct Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(realFileSystemResource, true))),
Arguments.of("Traditional / Indirect Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(realFileSystemResource, false))),
// TODO the cast to SeekableByteChannel is questionable
Arguments.of("Traditional / SeekableByteChannel", new SeekableByteChannelRangeWriter(() -> (SeekableByteChannel)realFileSystemResource.newReadableByteChannel())),
Arguments.of("Traditional / InputStream", new InputStreamRangeWriter(() -> realFileSystemResource.newInputStream())),
Arguments.of("Non-Default FS / Direct Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(nonDefaultFileSystemResource, true))),
Arguments.of("Non-Default FS / Indirect Buffer", new ByteBufferRangeWriter(BufferUtil.toBuffer(nonDefaultFileSystemResource, false))),
// TODO the cast to SeekableByteChannel is questionable
Arguments.of("Non-Default FS / SeekableByteChannel", new SeekableByteChannelRangeWriter(() -> (SeekableByteChannel)nonDefaultFileSystemResource.newReadableByteChannel())),
Arguments.of("Non-Default FS / InputStream", new InputStreamRangeWriter(() -> nonDefaultFileSystemResource.newInputStream()))
);
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("impls")
public void testSimpleRange(String description, RangeWriter rangeWriter) throws IOException
{
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 50);
assertThat("Range: 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("impls")
public void testSameRangeMultipleTimes(String description, RangeWriter rangeWriter) throws IOException
{
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 50);
assertThat("Range(a): 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 50);
assertThat("Range(b): 10 (len=50)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 60)));
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("impls")
public void testMultipleRangesOrdered(String description, RangeWriter rangeWriter) throws IOException
{
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 20);
assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 35, 10);
assertThat("Range(b): 35 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(35, 35 + 10)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 55, 10);
assertThat("Range(b): 55 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(55, 55 + 10)));
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("impls")
public void testMultipleRangesOverlapping(String description, RangeWriter rangeWriter) throws IOException
{
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 20);
assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 15, 20);
assertThat("Range(b): 15 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(15, 15 + 20)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 20, 20);
assertThat("Range(b): 20 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(20, 20 + 20)));
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("impls")
public void testMultipleRangesReverseOrder(String description, RangeWriter rangeWriter) throws IOException
{
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 55, 10);
assertThat("Range(b): 55 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(55, 55 + 10)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 35, 10);
assertThat("Range(b): 35 (len=10)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(35, 35 + 10)));
outputStream = new ByteArrayOutputStream();
rangeWriter.writeTo(outputStream, 10, 20);
assertThat("Range(a): 10 (len=20)", new String(outputStream.toByteArray(), UTF_8), is(DATA.substring(10, 10 + 20)));
}
}

View File

@ -333,7 +333,7 @@ public class ServletContextRequest extends ContextRequest implements Runnable
private boolean _requestedSessionIdFromCookie;
private Authentication _authentication;
private String _method;
private ServletMultiParts.Parts _multiParts;
private ServletMultiPartFormData.Parts _parts;
public static Session getSession(HttpSession httpSession)
{
@ -769,16 +769,16 @@ public class ServletContextRequest extends ContextRequest implements Runnable
String contentType = getContentType();
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
throw new ServletException("Unsupported Content-Type [%s], expected [%s]".formatted(contentType, MimeTypes.Type.MULTIPART_FORM_DATA.asString()));
if (_multiParts == null)
_multiParts = ServletMultiParts.from(this);
return _multiParts.getParts();
if (_parts == null)
_parts = ServletMultiPartFormData.from(this);
return _parts.getParts();
}
@Override
public Part getPart(String name) throws IOException, ServletException
{
getParts();
return _multiParts.getPart(name);
return _parts.getPart(name);
}
@Override

View File

@ -27,7 +27,7 @@ import jakarta.servlet.ServletContext;
import jakarta.servlet.http.Part;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiParts;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
@ -43,7 +43,7 @@ import org.eclipse.jetty.util.StringUtil;
*
* @see Parts
*/
public class ServletMultiParts
public class ServletMultiPartFormData
{
/**
* <p>Parses the request content assuming it is a multipart content,
@ -58,10 +58,10 @@ public class ServletMultiParts
{
try
{
MultiParts multiParts = (MultiParts)request.getAttribute(MultiParts.class.getName());
if (multiParts != null)
return new Parts(multiParts);
return new ServletMultiParts().parse(request);
MultiPartFormData formData = (MultiPartFormData)request.getAttribute(MultiPartFormData.class.getName());
if (formData != null)
return new Parts(formData);
return new ServletMultiPartFormData().parse(request);
}
catch (Throwable x)
{
@ -75,8 +75,11 @@ public class ServletMultiParts
if (config == null)
throw new IllegalStateException("No multipart configuration element");
String boundary = MultiParts.extractBoundary(request.getContentType());
MultiParts multiParts = new MultiParts(boundary);
String boundary = MultiPart.extractBoundary(request.getContentType());
if (boundary == null)
throw new IllegalStateException("No multipart boundary parameter in Content-Type");
MultiPartFormData formData = new MultiPartFormData(boundary);
File tmpDirFile = (File)request.getServletContext().getAttribute(ServletContext.TEMPDIR);
if (tmpDirFile == null)
@ -85,12 +88,12 @@ public class ServletMultiParts
if (!StringUtil.isBlank(fileLocation))
tmpDirFile = new File(fileLocation);
multiParts.setFilesDirectory(tmpDirFile.toPath());
multiParts.setMaxMemoryFileSize(config.getFileSizeThreshold());
multiParts.setMaxFileSize(config.getMaxFileSize());
multiParts.setMaxLength(config.getMaxRequestSize());
formData.setFilesDirectory(tmpDirFile.toPath());
formData.setMaxMemoryFileSize(config.getFileSizeThreshold());
formData.setMaxFileSize(config.getMaxFileSize());
formData.setMaxLength(config.getMaxRequestSize());
ConnectionMetaData connectionMetaData = request.getRequest().getConnectionMetaData();
multiParts.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
formData.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
Connection connection = connectionMetaData.getConnection();
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
@ -101,13 +104,13 @@ public class ServletMultiParts
int read = input.read(buffer);
if (read < 0)
{
multiParts.parse(Content.Chunk.EOF);
formData.parse(Content.Chunk.EOF);
break;
}
multiParts.parse(Content.Chunk.from(ByteBuffer.wrap(buffer, 0, read), false));
formData.parse(Content.Chunk.from(ByteBuffer.wrap(buffer, 0, read), false));
}
return new Parts(multiParts);
return new Parts(formData);
}
/**
@ -117,9 +120,9 @@ public class ServletMultiParts
{
private final List<Part> parts = new ArrayList<>();
public Parts(MultiParts multiParts)
public Parts(MultiPartFormData formData)
{
multiParts.join().forEach(part -> parts.add(new ServletPart(multiParts, part)));
formData.join().forEach(part -> parts.add(new ServletPart(formData, part)));
}
public Part getPart(String name)
@ -138,14 +141,14 @@ public class ServletMultiParts
private static class ServletPart implements Part
{
private final MultiParts _multiParts;
private final MultiPartFormData _formData;
private final MultiPart.Part _part;
private final long _length;
private final InputStream _input;
private ServletPart(MultiParts multiParts, MultiPart.Part part)
private ServletPart(MultiPartFormData formData, MultiPart.Part part)
{
_multiParts = multiParts;
_formData = formData;
_part = part;
Content.Source content = part.getContent();
_length = content.getLength();
@ -187,7 +190,7 @@ public class ServletMultiParts
{
Path filePath = Path.of(fileName);
if (!filePath.isAbsolute())
filePath = _multiParts.getFilesDirectory().resolve(filePath).normalize();
filePath = _formData.getFilesDirectory().resolve(filePath).normalize();
_part.writeTo(filePath);
}

View File

@ -45,7 +45,7 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiParts;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Server;
@ -235,12 +235,12 @@ public class MultiPartServletTest
assertThat(headers.get(HttpHeader.CONTENT_ENCODING), is("gzip"));
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
String boundary = MultiParts.extractBoundary(contentType);
MultiParts multiParts = new MultiParts(boundary);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartFormData formData = new MultiPartFormData(boundary);
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
multiParts.parse(Content.Chunk.from(ByteBuffer.wrap(IO.readBytes(inputStream)), true));
MultiParts.Parts parts = multiParts.join();
formData.parse(Content.Chunk.from(ByteBuffer.wrap(IO.readBytes(inputStream)), true));
MultiPartFormData.Parts parts = formData.join();
assertThat(parts.size(), is(1));
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));