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:
parent
2e01ed7e08
commit
b5332c38a9
|
@ -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;
|
|||
* </form>
|
||||
* </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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 header = headers.nextElement();
|
||||
StringTokenizer tok = new StringTokenizer(header, "=,", false);
|
||||
String t = null;
|
||||
try
|
||||
{
|
||||
// read all byte ranges for this header
|
||||
while (tok.hasMoreTokens())
|
||||
String prefix = "bytes=";
|
||||
for (String header : headers)
|
||||
{
|
||||
try
|
||||
{
|
||||
t = tok.nextToken().trim();
|
||||
if ("bytes".equals(t))
|
||||
String value = header.trim();
|
||||
if (!value.startsWith(prefix))
|
||||
continue;
|
||||
|
||||
value = value.substring(prefix.length());
|
||||
for (String range : value.split(","))
|
||||
{
|
||||
range = range.trim();
|
||||
long first = -1;
|
||||
long last = -1;
|
||||
int dash = t.indexOf('-');
|
||||
if (dash < 0 || t.indexOf("-", dash + 1) >= 0)
|
||||
int dash = range.indexOf('-');
|
||||
if (dash < 0 || range.indexOf("-", dash + 1) >= 0)
|
||||
{
|
||||
ranges = null;
|
||||
LOG.warn("Bad range format: {}", t);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("bad range format: {}", range);
|
||||
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());
|
||||
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)
|
||||
{
|
||||
ranges = null;
|
||||
LOG.warn("Bad range format: {}", t);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("bad range format: {}", range);
|
||||
break;
|
||||
}
|
||||
|
||||
if (last == 0)
|
||||
continue;
|
||||
|
||||
// This is a suffix range
|
||||
first = Math.max(0, size - last);
|
||||
// This is a suffix range of the form "-20".
|
||||
first = Math.max(0, end - last + 1);
|
||||
last = end;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Range starts after end
|
||||
if (first >= size)
|
||||
// Range starts after end.
|
||||
if (first > end)
|
||||
continue;
|
||||
|
||||
if (last == -1)
|
||||
last = end;
|
||||
else if (last >= end)
|
||||
if (last == -1 || last > end)
|
||||
last = end;
|
||||
}
|
||||
|
||||
if (last < first)
|
||||
{
|
||||
ranges = null;
|
||||
LOG.warn("Bad range format: {}", t);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("bad range format: {}", range);
|
||||
break;
|
||||
}
|
||||
|
||||
ByteRange range = new ByteRange(first, last);
|
||||
ByteRange byteRange = 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();
|
||||
ranges.add(byteRange);
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("could not parse range {}", header, x);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
if (!coalesced)
|
||||
ranges.add(range);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
ranges = null;
|
||||
LOG.warn("Bad range format: {}", t);
|
||||
LOG.trace("IGNORED", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ranges = null;
|
||||
LOG.warn("Bad range format: {}", t);
|
||||
LOG.trace("IGNORED", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (ranges == null)
|
||||
return List.of();
|
||||
if (ranges.size() == 1)
|
||||
return ranges;
|
||||
}
|
||||
|
||||
public static String to416HeaderRangeString(long size)
|
||||
// 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)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(40);
|
||||
sb.append("bytes */");
|
||||
sb.append(size);
|
||||
return sb.toString();
|
||||
ByteRange range2 = ranges.get(i);
|
||||
if (range1.overlaps(range2))
|
||||
{
|
||||
range1 = range1.coalesce(range2);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.add(range1);
|
||||
range1 = range2;
|
||||
}
|
||||
}
|
||||
result.add(range1);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* <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)
|
||||
{
|
||||
return "bytes */" + length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
* <p>The multipart/form-data specific content source.</p>
|
||||
*
|
||||
* @return a new {@link MultiPart.ContentSource} with all the parts of this object
|
||||
* @see MultiPart.AbstractContentSource
|
||||
*/
|
||||
public MultiPart.ContentSource toContentSource()
|
||||
public static class ContentSource extends MultiPart.AbstractContentSource
|
||||
{
|
||||
MultiPart.ContentSource result = new MultiPart.ContentSource(getBoundary());
|
||||
parts.forEach(result::addPart);
|
||||
result.close();
|
||||
return result;
|
||||
public ContentSource(String boundary)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
parts.stream()
|
||||
.filter(part -> part instanceof MultiPart.PathPart)
|
||||
.map(MultiPart.PathPart.class::cast)
|
||||
.forEach(MultiPart.PathPart::delete);
|
||||
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();
|
|
@ -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---");
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
partByteBuffers.add(BufferUtil.copy(chunk.getByteBuffer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
{
|
||||
MultiPart.Part newPart = new MultiPart.ByteBufferPart(part.getName(), part.getFileName(), part.getHeaders(), byteBuffers);
|
||||
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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,22 +44,24 @@ 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Files.isRegularFile(path))
|
||||
throw new NoSuchFileException(path.toString());
|
||||
|
@ -68,6 +71,11 @@ public class PathContentSource implements Content.Source
|
|||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
@ -457,6 +456,7 @@ public class ResourceService
|
|||
* 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);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is only a single valid range (must be satisfiable
|
||||
// since were here now), send that range with a 216 response
|
||||
// Parse the satisfiable ranges.
|
||||
List<ByteRange> ranges = ByteRange.parse(reqRanges, contentLength);
|
||||
|
||||
// If there are no satisfiable ranges, send a 416 response.
|
||||
if (ranges.isEmpty())
|
||||
{
|
||||
putHeaders(response, content, NO_CONTENT_LENGTH);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_RANGE, ByteRange.toNonSatisfiableHeaderValue(contentLength));
|
||||
Response.writeError(request, response, callback, HttpStatus.RANGE_NOT_SATISFIABLE_416);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is only a single valid range, send that range with a 206 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;
|
||||
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;
|
||||
}
|
||||
|
||||
// multiple non-overlapping valid ranges cause a multipart
|
||||
// 216 response which does not require an overall
|
||||
// content-length header
|
||||
//
|
||||
// There are multiple non-overlapping ranges, send a multipart/byteranges 206 response.
|
||||
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();
|
||||
*/
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void writeHttpPartialContent(Request request, Response response, Callback callback, HttpContent content, ByteRange singleSatisfiableRange)
|
||||
{
|
||||
// TODO: implement this
|
||||
}
|
||||
|
||||
protected void writeHttpError(Request request, Response response, Callback callback, int statusCode)
|
||||
{
|
||||
Response.writeError(request, response, callback, statusCode);
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue