Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-websocket-autodemanding
This commit is contained in:
commit
2d11517dd6
|
@ -33,6 +33,7 @@ import java.util.function.Consumer;
|
|||
import org.eclipse.jetty.client.Response.Listener;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.StaticException;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -301,7 +302,7 @@ public class InputStreamResponseListener extends Listener.Adapter
|
|||
break;
|
||||
|
||||
if (failure != null)
|
||||
throw toIOException(failure);
|
||||
throw new IOException(failure);
|
||||
|
||||
if (closed)
|
||||
throw new AsynchronousCloseException();
|
||||
|
@ -327,14 +328,6 @@ public class InputStreamResponseListener extends Listener.Adapter
|
|||
}
|
||||
}
|
||||
|
||||
private IOException toIOException(Throwable failure)
|
||||
{
|
||||
if (failure instanceof IOException)
|
||||
return (IOException)failure;
|
||||
else
|
||||
return new IOException(failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
|
|
|
@ -69,7 +69,7 @@ public class MultiPartRequestContent extends MultiPartFormData.ContentSource imp
|
|||
if (headers.contains(HttpHeader.CONTENT_TYPE))
|
||||
return headers;
|
||||
|
||||
Content.Source partContent = part.getContent();
|
||||
Content.Source partContent = part.getContentSource();
|
||||
if (partContent instanceof Request.Content requestContent)
|
||||
{
|
||||
String contentType = requestContent.getContentType();
|
||||
|
|
|
@ -135,7 +135,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
int equal = contentType.lastIndexOf('=');
|
||||
Charset charset = Charset.forName(contentType.substring(equal + 1));
|
||||
assertEquals(encoding, charset);
|
||||
assertEquals(value, Content.Source.asString(part.getContent(), charset));
|
||||
assertEquals(value, Content.Source.asString(part.getContentSource(), charset));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -169,7 +169,7 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
MultiPart.Part part = parts.iterator().next();
|
||||
assertEquals(name, part.getName());
|
||||
assertEquals("text/plain", part.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(part.getContent()).array());
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(part.getContentSource()).array());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -221,8 +221,8 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
assertEquals(name, part.getName());
|
||||
assertEquals(contentType, part.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(fileName, part.getFileName());
|
||||
assertEquals(data.length, part.getContent().getLength());
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(part.getContent()).array());
|
||||
assertEquals(data.length, part.getContentSource().getLength());
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(part.getContentSource()).array());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -277,8 +277,8 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
assertEquals(name, part.getName());
|
||||
assertEquals(contentType, part.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(tmpPath.getFileName().toString(), part.getFileName());
|
||||
assertEquals(Files.size(tmpPath), part.getContent().getLength());
|
||||
assertEquals(data, Content.Source.asString(part.getContent(), encoding));
|
||||
assertEquals(Files.size(tmpPath), part.getContentSource().getLength());
|
||||
assertEquals(data, Content.Source.asString(part.getContentSource(), encoding));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -329,14 +329,14 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
|
||||
assertEquals(field, fieldPart.getName());
|
||||
assertEquals(contentType, fieldPart.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(value, Content.Source.asString(fieldPart.getContent(), encoding));
|
||||
assertEquals(value, Content.Source.asString(fieldPart.getContentSource(), encoding));
|
||||
assertEquals(headerValue, fieldPart.getHeaders().get(headerName));
|
||||
|
||||
assertEquals(fileField, filePart.getName());
|
||||
assertEquals("application/octet-stream", filePart.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(tmpPath.getFileName().toString(), filePart.getFileName());
|
||||
assertEquals(Files.size(tmpPath), filePart.getContent().getLength());
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(filePart.getContent()).array());
|
||||
assertEquals(Files.size(tmpPath), filePart.getContentSource().getLength());
|
||||
assertArrayEquals(data, Content.Source.asByteBuffer(filePart.getContentSource()).array());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -373,11 +373,11 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
|
|||
MultiPart.Part fieldPart = parts.get(0);
|
||||
MultiPart.Part filePart = parts.get(1);
|
||||
|
||||
assertEquals(value, Content.Source.asString(fieldPart.getContent(), encoding));
|
||||
assertEquals(value, Content.Source.asString(fieldPart.getContentSource(), encoding));
|
||||
assertEquals("file", filePart.getName());
|
||||
assertEquals("application/octet-stream", filePart.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals("fileName", filePart.getFileName());
|
||||
assertArrayEquals(fileData, Content.Source.asByteBuffer(filePart.getContent()).array());
|
||||
assertArrayEquals(fileData, Content.Source.asByteBuffer(filePart.getContentSource()).array());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -304,6 +304,12 @@ public class HttpStreamOverFCGI implements HttpStream
|
|||
return _committed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable consumeAvailable()
|
||||
{
|
||||
return HttpStream.consumeAvailable(this, _httpChannel.getConnectionMetaData().getHttpConfiguration());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
|
|
|
@ -18,7 +18,6 @@ import java.io.EOFException;
|
|||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.Buffer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
|
@ -40,6 +39,7 @@ import org.eclipse.jetty.util.BufferUtil;
|
|||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.SearchPattern;
|
||||
import org.eclipse.jetty.util.StaticException;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.Utf8StringBuilder;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
|
@ -65,6 +65,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||
*/
|
||||
public class MultiPart
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MultiPart.class);
|
||||
private static final int MAX_BOUNDARY_LENGTH = 70;
|
||||
|
||||
private MultiPart()
|
||||
|
@ -117,17 +118,33 @@ public class MultiPart
|
|||
* <p>A part has an optional name, an optional fileName,
|
||||
* optional headers and an optional content.</p>
|
||||
*/
|
||||
public abstract static class Part
|
||||
public abstract static class Part implements Closeable
|
||||
{
|
||||
private static final Throwable CLOSE_EXCEPTION = new StaticException("Closed");
|
||||
|
||||
private final String name;
|
||||
private final String fileName;
|
||||
private final HttpFields fields;
|
||||
private Content.Source contentSource;
|
||||
private Path path;
|
||||
private boolean temporary = true;
|
||||
|
||||
public Part(String name, String fileName, HttpFields fields)
|
||||
{
|
||||
this(name, fileName, fields, null);
|
||||
}
|
||||
|
||||
private Part(String name, String fileName, HttpFields fields, Path path)
|
||||
{
|
||||
this.name = name;
|
||||
this.fileName = fileName;
|
||||
this.fields = fields != null ? fields : HttpFields.EMPTY;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
private Path getPath()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,7 +175,8 @@ public class MultiPart
|
|||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the content of this part.</p>
|
||||
* <p>Returns the content of this part as a {@link Content.Source}.</p>
|
||||
* <p>Calling this method multiple times will return the same instance, which can only be consumed once.</p>
|
||||
* <p>The content type and content encoding are specified in this part's
|
||||
* {@link #getHeaders() headers}.</p>
|
||||
* <p>The content encoding may be specified by the part named {@code _charset_},
|
||||
|
@ -166,8 +184,34 @@ public class MultiPart
|
|||
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>.</p>
|
||||
*
|
||||
* @return the content of this part
|
||||
* @see #newContentSource()
|
||||
*/
|
||||
public abstract Content.Source getContent();
|
||||
public Content.Source getContentSource()
|
||||
{
|
||||
if (contentSource == null)
|
||||
contentSource = newContentSource();
|
||||
return contentSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the content of this part as a new {@link Content.Source}</p>
|
||||
* <p>If the content is reproducible, invoking this method multiple times will return
|
||||
* a different independent instance for every invocation.</p>
|
||||
* <p>If the content is not reproducible, subsequent calls to this method will return null.</p>
|
||||
* <p>The content type and content encoding are specified in this part's {@link #getHeaders() headers}.</p>
|
||||
* <p>The content encoding may be specified by the part named {@code _charset_},
|
||||
* as specified in
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>.</p>
|
||||
*
|
||||
* @return the content of this part as a new {@link Content.Source} or null if the content cannot be consumed multiple times.
|
||||
* @see #getContentSource()
|
||||
*/
|
||||
public abstract Content.Source newContentSource();
|
||||
|
||||
public long getLength()
|
||||
{
|
||||
return getContentSource().getLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the content of this part as a string.</p>
|
||||
|
@ -191,7 +235,7 @@ public class MultiPart
|
|||
Charset charset = defaultCharset != null ? defaultCharset : UTF_8;
|
||||
if (charsetName != null)
|
||||
charset = Charset.forName(charsetName);
|
||||
return Content.Source.asString(getContent(), charset);
|
||||
return Content.Source.asString(newContentSource(), charset);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
|
@ -215,9 +259,46 @@ public class MultiPart
|
|||
*/
|
||||
public void writeTo(Path path) throws IOException
|
||||
{
|
||||
try (OutputStream out = Files.newOutputStream(path))
|
||||
if (this.path == null)
|
||||
{
|
||||
IO.copy(Content.Source.asInputStream(getContent()), out);
|
||||
try (OutputStream out = Files.newOutputStream(path))
|
||||
{
|
||||
IO.copy(Content.Source.asInputStream(newContentSource()), out);
|
||||
}
|
||||
this.path = path;
|
||||
this.temporary = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.path = Files.move(this.path, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
this.temporary = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void delete() throws IOException
|
||||
{
|
||||
if (this.path != null)
|
||||
Files.delete(this.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
fail(CLOSE_EXCEPTION);
|
||||
}
|
||||
|
||||
public void fail(Throwable t)
|
||||
{
|
||||
try
|
||||
{
|
||||
getContentSource().fail(t);
|
||||
if (temporary)
|
||||
delete();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Error closing part {}", this, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,8 +309,7 @@ public class MultiPart
|
|||
*/
|
||||
public static class ByteBufferPart extends Part
|
||||
{
|
||||
private final Content.Source content;
|
||||
private final long length;
|
||||
private final List<ByteBuffer> content;
|
||||
|
||||
public ByteBufferPart(String name, String fileName, HttpFields fields, ByteBuffer... buffers)
|
||||
{
|
||||
|
@ -239,14 +319,13 @@ public class MultiPart
|
|||
public ByteBufferPart(String name, String fileName, HttpFields fields, List<ByteBuffer> content)
|
||||
{
|
||||
super(name, fileName, fields);
|
||||
this.content = new ByteBufferContentSource(content);
|
||||
this.length = content.stream().mapToLong(Buffer::remaining).sum();
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Source getContent()
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
return content;
|
||||
return new ByteBufferContentSource(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -257,7 +336,7 @@ public class MultiPart
|
|||
hashCode(),
|
||||
getName(),
|
||||
getFileName(),
|
||||
length
|
||||
getLength()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -267,20 +346,29 @@ public class MultiPart
|
|||
*/
|
||||
public static class ChunksPart extends Part
|
||||
{
|
||||
private final Content.Source content;
|
||||
private final long length;
|
||||
private final List<Content.Chunk> content;
|
||||
|
||||
public ChunksPart(String name, String fileName, HttpFields fields, List<Content.Chunk> content)
|
||||
{
|
||||
super(name, fileName, fields);
|
||||
this.content = new ChunksContentSource(content);
|
||||
this.length = content.stream().mapToLong(c -> c.getByteBuffer().remaining()).sum();
|
||||
this.content = Objects.requireNonNull(content);
|
||||
content.forEach(Content.Chunk::retain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Source getContent()
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
return content;
|
||||
List<Content.Chunk> newChunks = content.stream()
|
||||
.map(chunk -> Content.Chunk.from(chunk.getByteBuffer().slice(), chunk.isLast()))
|
||||
.toList();
|
||||
return new ChunksContentSource(newChunks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
super.close();
|
||||
content.forEach(Content.Chunk::release);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -291,7 +379,7 @@ public class MultiPart
|
|||
hashCode(),
|
||||
getName(),
|
||||
getFileName(),
|
||||
length
|
||||
getLength()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -301,41 +389,20 @@ public class MultiPart
|
|||
*/
|
||||
public static class PathPart extends Part
|
||||
{
|
||||
private final PathContentSource content;
|
||||
|
||||
public PathPart(String name, String fileName, HttpFields fields, Path path)
|
||||
{
|
||||
super(name, fileName, fields);
|
||||
this.content = new PathContentSource(path);
|
||||
super(name, fileName, fields, path);
|
||||
}
|
||||
|
||||
public Path getPath()
|
||||
{
|
||||
return content.getPath();
|
||||
return super.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Source getContent()
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Path path) throws IOException
|
||||
{
|
||||
Files.move(getPath(), path, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public void delete()
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.delete(getPath());
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new UncheckedIOException(x);
|
||||
}
|
||||
return new PathContentSource(getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -356,18 +423,20 @@ public class MultiPart
|
|||
*/
|
||||
public static class ContentSourcePart extends Part
|
||||
{
|
||||
private final Content.Source content;
|
||||
private Content.Source content;
|
||||
|
||||
public ContentSourcePart(String name, String fileName, HttpFields fields, Content.Source content)
|
||||
{
|
||||
super(name, fileName, fields);
|
||||
this.content = content;
|
||||
this.content = Objects.requireNonNull(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Source getContent()
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
return content;
|
||||
Content.Source c = content;
|
||||
content = null;
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -378,7 +447,7 @@ public class MultiPart
|
|||
hashCode(),
|
||||
getName(),
|
||||
getFileName(),
|
||||
content.getLength()
|
||||
getLength()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -628,7 +697,7 @@ public class MultiPart
|
|||
}
|
||||
case CONTENT ->
|
||||
{
|
||||
Content.Chunk chunk = part.getContent().read();
|
||||
Content.Chunk chunk = part.getContentSource().read();
|
||||
if (chunk == null || chunk instanceof Content.Chunk.Error)
|
||||
yield chunk;
|
||||
if (!chunk.isLast())
|
||||
|
@ -667,7 +736,7 @@ public class MultiPart
|
|||
|
||||
if (state == State.CONTENT)
|
||||
{
|
||||
part.getContent().demand(() ->
|
||||
part.getContentSource().demand(() ->
|
||||
{
|
||||
try (AutoLock ignoredAgain = lock.lock())
|
||||
{
|
||||
|
@ -688,18 +757,21 @@ public class MultiPart
|
|||
@Override
|
||||
public void fail(Throwable failure)
|
||||
{
|
||||
Part part;
|
||||
List<Part> drained;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (closed && parts.isEmpty())
|
||||
return;
|
||||
if (errorChunk != null)
|
||||
return;
|
||||
errorChunk = Content.Chunk.from(failure);
|
||||
drained = List.copyOf(parts);
|
||||
parts.clear();
|
||||
part = this.part;
|
||||
this.part = null;
|
||||
}
|
||||
drained.forEach(part -> part.getContent().fail(failure));
|
||||
if (part != null)
|
||||
part.fail(failure);
|
||||
drained.forEach(p -> p.fail(failure));
|
||||
invoker.run(this::invokeDemandCallback);
|
||||
}
|
||||
|
||||
|
@ -763,6 +835,8 @@ public class MultiPart
|
|||
private int trailingWhiteSpaces;
|
||||
private String fieldName;
|
||||
private String fieldValue;
|
||||
private long maxParts = 1000;
|
||||
private int numParts;
|
||||
|
||||
public Parser(String boundary, Listener listener)
|
||||
{
|
||||
|
@ -794,6 +868,22 @@ public class MultiPart
|
|||
this.partHeadersMaxLength = partHeadersMaxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
|
||||
*/
|
||||
public long getMaxParts()
|
||||
{
|
||||
return maxParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
|
||||
*/
|
||||
public void setMaxParts(long maxParts)
|
||||
{
|
||||
this.maxParts = maxParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Resets this parser to make it ready to parse again a multipart/form-data content.</p>
|
||||
*/
|
||||
|
@ -852,6 +942,10 @@ public class MultiPart
|
|||
}
|
||||
else if (type == HttpTokens.Type.LF)
|
||||
{
|
||||
numParts++;
|
||||
if (maxParts >= 0 && numParts > maxParts)
|
||||
throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", numParts, maxParts));
|
||||
|
||||
notifyPartBegin();
|
||||
state = State.HEADER_START;
|
||||
trailingWhiteSpaces = 0;
|
||||
|
|
|
@ -244,7 +244,8 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
|
|||
*/
|
||||
public static class Part extends MultiPart.Part
|
||||
{
|
||||
private final PathContentSource content;
|
||||
private final Path path;
|
||||
private final ByteRange byteRange;
|
||||
|
||||
public Part(String contentType, Path path, ByteRange byteRange, long contentLength)
|
||||
{
|
||||
|
@ -255,13 +256,14 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
|
|||
public Part(HttpFields headers, Path path, ByteRange byteRange)
|
||||
{
|
||||
super(null, null, headers);
|
||||
content = new PathContentSource(path, byteRange);
|
||||
this.path = path;
|
||||
this.byteRange = byteRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Source getContent()
|
||||
public Content.Source newContentSource()
|
||||
{
|
||||
return content;
|
||||
return new PathContentSource(path, byteRange);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,6 +294,7 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
|
|||
public void onPart(String name, String fileName, HttpFields headers)
|
||||
{
|
||||
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
}
|
||||
|
||||
|
@ -311,16 +314,20 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
|
|||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> toFail;
|
||||
List<MultiPart.Part> partsToFail;
|
||||
List<Content.Chunk> partChunksToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
toFail = new ArrayList<>(parts);
|
||||
partsToFail = new ArrayList<>(parts);
|
||||
parts.clear();
|
||||
partChunksToFail = new ArrayList<>(partChunks);
|
||||
partChunks.clear();
|
||||
}
|
||||
toFail.forEach(part -> part.getContent().fail(cause));
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
partChunksToFail.forEach(Content.Chunk::release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import java.util.Objects;
|
|||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.Retainable;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -273,6 +275,22 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public long getMaxParts()
|
||||
{
|
||||
return parser.getMaxParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
|
||||
*/
|
||||
public void setMaxParts(long maxParts)
|
||||
{
|
||||
parser.setMaxParts(maxParts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean completeExceptionally(Throwable failure)
|
||||
{
|
||||
|
@ -290,23 +308,18 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
* <p>An ordered list of {@link MultiPart.Part}s that can
|
||||
* be accessed by index or by name, or iterated over.</p>
|
||||
*/
|
||||
public static class Parts implements Iterable<MultiPart.Part>
|
||||
public class Parts implements Iterable<MultiPart.Part>, Closeable
|
||||
{
|
||||
private final String boundary;
|
||||
private final List<MultiPart.Part> parts;
|
||||
|
||||
private Parts(String boundary, List<MultiPart.Part> parts)
|
||||
private Parts(List<MultiPart.Part> parts)
|
||||
{
|
||||
this.boundary = boundary;
|
||||
this.parts = parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
public MultiPartFormData getMultiPartFormData()
|
||||
{
|
||||
return boundary;
|
||||
return MultiPartFormData.this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -364,6 +377,15 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
{
|
||||
return parts.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
for (MultiPart.Part p : parts)
|
||||
{
|
||||
IO.close(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -426,19 +448,27 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
memoryFileSize += buffer.remaining();
|
||||
if (memoryFileSize > maxMemoryFileSize)
|
||||
{
|
||||
// Must save to disk.
|
||||
if (ensureFileChannel())
|
||||
try
|
||||
{
|
||||
// Write existing memory chunks.
|
||||
for (Content.Chunk c : partChunks)
|
||||
// Must save to disk.
|
||||
if (ensureFileChannel())
|
||||
{
|
||||
if (!write(c.getByteBuffer()))
|
||||
return;
|
||||
// Write existing memory chunks.
|
||||
for (Content.Chunk c : partChunks)
|
||||
{
|
||||
write(c.getByteBuffer());
|
||||
}
|
||||
}
|
||||
write(buffer);
|
||||
if (chunk.isLast())
|
||||
close();
|
||||
}
|
||||
write(buffer);
|
||||
if (chunk.isLast())
|
||||
close();
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
}
|
||||
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -448,24 +478,15 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
partChunks.add(chunk);
|
||||
}
|
||||
|
||||
private boolean write(ByteBuffer buffer)
|
||||
private void write(ByteBuffer buffer) throws Exception
|
||||
{
|
||||
try
|
||||
int remaining = buffer.remaining();
|
||||
while (remaining > 0)
|
||||
{
|
||||
int remaining = buffer.remaining();
|
||||
while (remaining > 0)
|
||||
{
|
||||
int written = fileChannel.write(buffer);
|
||||
if (written == 0)
|
||||
throw new NonWritableChannelException();
|
||||
remaining -= written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
onFailure(x);
|
||||
return false;
|
||||
int written = fileChannel.write(buffer);
|
||||
if (written == 0)
|
||||
throw new NonWritableChannelException();
|
||||
remaining -= written;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,6 +517,7 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
memoryFileSize = 0;
|
||||
filePath = null;
|
||||
fileChannel = null;
|
||||
partChunks.forEach(Content.Chunk::release);
|
||||
partChunks.clear();
|
||||
// Store the new part.
|
||||
try (AutoLock ignored = lock.lock())
|
||||
|
@ -508,7 +530,7 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
public void onComplete()
|
||||
{
|
||||
super.onComplete();
|
||||
complete(new Parts(getBoundary(), getParts()));
|
||||
complete(new Parts(getParts()));
|
||||
}
|
||||
|
||||
private List<MultiPart.Part> getParts()
|
||||
|
@ -528,22 +550,20 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
|
||||
private void fail(Throwable cause)
|
||||
{
|
||||
List<MultiPart.Part> toFail;
|
||||
List<MultiPart.Part> partsToFail;
|
||||
List<Content.Chunk> partChunksToFail;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (failure != null)
|
||||
return;
|
||||
failure = cause;
|
||||
toFail = new ArrayList<>(parts);
|
||||
partsToFail = new ArrayList<>(parts);
|
||||
parts.clear();
|
||||
partChunksToFail = new ArrayList<>(partChunks);
|
||||
partChunks.clear();
|
||||
}
|
||||
for (MultiPart.Part part : toFail)
|
||||
{
|
||||
if (part instanceof MultiPart.PathPart pathPart)
|
||||
pathPart.delete();
|
||||
else
|
||||
part.getContent().fail(cause);
|
||||
}
|
||||
partsToFail.forEach(p -> p.fail(cause));
|
||||
partChunksToFail.forEach(Retainable::release);
|
||||
close();
|
||||
delete();
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ import static org.hamcrest.Matchers.containsString;
|
|||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class MultiPartCaptureTest
|
||||
{
|
||||
|
@ -245,7 +244,7 @@ public class MultiPartCaptureTest
|
|||
List<MultiPart.Part> charSetParts = allParts.get("_charset_");
|
||||
if (charSetParts != null)
|
||||
{
|
||||
defaultCharset = Promise.Completable.<String>with(p -> Content.Source.asString(charSetParts.get(0).getContent(), StandardCharsets.US_ASCII, p))
|
||||
defaultCharset = Promise.Completable.<String>with(p -> Content.Source.asString(charSetParts.get(0).getContentSource(), StandardCharsets.US_ASCII, p))
|
||||
.get();
|
||||
}
|
||||
|
||||
|
@ -255,8 +254,7 @@ 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);
|
||||
assertTrue(part.getContent().rewind());
|
||||
String partContent = Content.Source.asString(part.getContent(), Charset.forName(charset));
|
||||
String partContent = Content.Source.asString(part.newContentSource(), Charset.forName(charset));
|
||||
assertThat("Part[" + expected.name + "].contents", partContent, containsString(expected.value));
|
||||
}
|
||||
|
||||
|
@ -276,8 +274,7 @@ public class MultiPartCaptureTest
|
|||
assertThat("Part[" + expected.name + "]", parts, is(notNullValue()));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
assertTrue(part.getContent().rewind());
|
||||
try (InputStream partInputStream = Content.Source.asInputStream(part.getContent());
|
||||
try (InputStream partInputStream = Content.Source.asInputStream(part.newContentSource());
|
||||
DigestOutputStream digester = new DigestOutputStream(OutputStream.nullOutputStream(), digest))
|
||||
{
|
||||
IO.copy(partInputStream, digester);
|
||||
|
|
|
@ -189,25 +189,25 @@ public class MultiPartFormDataTest
|
|||
|
||||
MultiPart.Part fileName = parts.getFirst("fileName");
|
||||
assertThat(fileName, notNullValue());
|
||||
Content.Source partContent = fileName.getContent();
|
||||
Content.Source partContent = fileName.getContentSource();
|
||||
assertThat(partContent.getLength(), is(3L));
|
||||
assertThat(Content.Source.asString(partContent), is("abc"));
|
||||
|
||||
MultiPart.Part desc = parts.getFirst("desc");
|
||||
assertThat(desc, notNullValue());
|
||||
partContent = desc.getContent();
|
||||
partContent = desc.getContentSource();
|
||||
assertThat(partContent.getLength(), is(3L));
|
||||
assertThat(Content.Source.asString(partContent), is("123"));
|
||||
|
||||
MultiPart.Part title = parts.getFirst("title");
|
||||
assertThat(title, notNullValue());
|
||||
partContent = title.getContent();
|
||||
partContent = title.getContentSource();
|
||||
assertThat(partContent.getLength(), is(3L));
|
||||
assertThat(Content.Source.asString(partContent), is("ttt"));
|
||||
|
||||
MultiPart.Part datafile = parts.getFirst("datafile5239138112980980385.txt");
|
||||
assertThat(datafile, notNullValue());
|
||||
partContent = datafile.getContent();
|
||||
partContent = datafile.getContentSource();
|
||||
assertThat(partContent.getLength(), is(3L));
|
||||
assertThat(Content.Source.asString(partContent), is("000"));
|
||||
}
|
||||
|
@ -275,11 +275,11 @@ public class MultiPartFormDataTest
|
|||
assertThat(parts.size(), is(2));
|
||||
MultiPart.Part part1 = parts.getFirst("field1");
|
||||
assertThat(part1, notNullValue());
|
||||
Content.Source partContent = part1.getContent();
|
||||
Content.Source partContent = part1.getContentSource();
|
||||
assertThat(Content.Source.asString(partContent), is("Joe Blow"));
|
||||
MultiPart.Part part2 = parts.getFirst("stuff");
|
||||
assertThat(part2, notNullValue());
|
||||
partContent = part2.getContent();
|
||||
partContent = part2.getContentSource();
|
||||
assertThat(Content.Source.asString(partContent), is("aaaabbbbb"));
|
||||
}
|
||||
|
||||
|
@ -312,7 +312,7 @@ public class MultiPartFormDataTest
|
|||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part2 = parts.getFirst("stuff");
|
||||
assertThat(part2, notNullValue());
|
||||
Content.Source partContent = part2.getContent();
|
||||
Content.Source partContent = part2.getContentSource();
|
||||
assertThat(Content.Source.asString(partContent), is("aaaabbbbb"));
|
||||
}
|
||||
|
||||
|
@ -340,7 +340,7 @@ public class MultiPartFormDataTest
|
|||
assertThat(part, instanceOf(MultiPart.PathPart.class));
|
||||
MultiPart.PathPart pathPart = (MultiPart.PathPart)part;
|
||||
assertTrue(Files.exists(pathPart.getPath()));
|
||||
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", Content.Source.asString(part.getContent()));
|
||||
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", Content.Source.asString(part.getContentSource()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -422,13 +422,13 @@ public class MultiPartFormDataTest
|
|||
|
||||
MultiPart.Part part1 = parts.get(0);
|
||||
assertThat(part1, instanceOf(MultiPart.ChunksPart.class));
|
||||
assertEquals(chunk, Content.Source.asString(part1.getContent()));
|
||||
assertEquals(chunk, Content.Source.asString(part1.getContentSource()));
|
||||
|
||||
MultiPart.Part part2 = parts.get(1);
|
||||
assertThat(part2, instanceOf(MultiPart.PathPart.class));
|
||||
MultiPart.PathPart pathPart2 = (MultiPart.PathPart)part2;
|
||||
assertTrue(Files.exists(pathPart2.getPath()));
|
||||
assertEquals(chunk.repeat(4), Content.Source.asString(part2.getContent()));
|
||||
assertEquals(chunk.repeat(4), Content.Source.asString(part2.getContentSource()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -361,11 +361,11 @@ public class MultiPartTest
|
|||
|
||||
MultiPart.Part part1 = listener.parts.get(0);
|
||||
assertEquals("value", part1.getHeaders().get("name"));
|
||||
assertEquals("Hello", Content.Source.asString(part1.getContent()));
|
||||
assertEquals("Hello", Content.Source.asString(part1.getContentSource()));
|
||||
|
||||
MultiPart.Part part2 = listener.parts.get(1);
|
||||
assertEquals("9001", part2.getHeaders().get("powerLevel"));
|
||||
assertEquals("secondary\r\ncontent", Content.Source.asString(part2.getContent()));
|
||||
assertEquals("secondary\r\ncontent", Content.Source.asString(part2.getContentSource()));
|
||||
|
||||
assertEquals(0, data.remaining());
|
||||
}
|
||||
|
@ -397,11 +397,11 @@ public class MultiPartTest
|
|||
|
||||
MultiPart.Part part1 = listener.parts.get(0);
|
||||
assertEquals("value", part1.getHeaders().get("name"));
|
||||
assertEquals("Hello", Content.Source.asString(part1.getContent()));
|
||||
assertEquals("Hello", Content.Source.asString(part1.getContentSource()));
|
||||
|
||||
MultiPart.Part part2 = listener.parts.get(1);
|
||||
assertEquals("9001", part2.getHeaders().get("powerLevel"));
|
||||
assertEquals("secondary\ncontent", Content.Source.asString(part2.getContent()));
|
||||
assertEquals("secondary\ncontent", Content.Source.asString(part2.getContentSource()));
|
||||
|
||||
assertEquals(0, data.remaining());
|
||||
}
|
||||
|
@ -457,7 +457,7 @@ public class MultiPartTest
|
|||
assertEquals(1, listener.parts.size());
|
||||
MultiPart.Part part = listener.parts.get(0);
|
||||
assertEquals("value", part.getHeaders().get("name"));
|
||||
assertEquals("", Content.Source.asString(part.getContent()));
|
||||
assertEquals("", Content.Source.asString(part.getContentSource()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -477,7 +477,7 @@ public class MultiPartTest
|
|||
assertEquals(1, listener.parts.size());
|
||||
MultiPart.Part part = listener.parts.get(0);
|
||||
assertEquals("value", part.getHeaders().get("name"));
|
||||
assertEquals("", Content.Source.asString(part.getContent()));
|
||||
assertEquals("", Content.Source.asString(part.getContentSource()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -508,7 +508,7 @@ public class MultiPartTest
|
|||
assertEquals(1, listener.parts.size());
|
||||
MultiPart.Part part = listener.parts.get(0);
|
||||
assertEquals("value", part.getHeaders().get("name"));
|
||||
assertThat(Content.Source.asString(part.getContent()), is("""
|
||||
assertThat(Content.Source.asString(part.getContentSource()), is("""
|
||||
Hello\r
|
||||
this is not a --BOUNDARY\r
|
||||
that's a boundary"""));
|
||||
|
@ -532,7 +532,7 @@ public class MultiPartTest
|
|||
assertThat(epilogueBuffer.remaining(), is(0));
|
||||
assertEquals(1, listener.parts.size());
|
||||
MultiPart.Part part = listener.parts.get(0);
|
||||
assertThat(Content.Source.asByteBuffer(part.getContent()), is(ByteBuffer.wrap(random)));
|
||||
assertThat(Content.Source.asByteBuffer(part.getContentSource()), is(ByteBuffer.wrap(random)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -556,7 +556,7 @@ public class MultiPartTest
|
|||
assertEquals(1, listener.parts.size());
|
||||
MultiPart.Part part = listener.parts.get(0);
|
||||
assertEquals("value", part.getHeaders().get("name"));
|
||||
assertEquals("Hello", Content.Source.asString(part.getContent()));
|
||||
assertEquals("Hello", Content.Source.asString(part.getContentSource()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -564,7 +564,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
|
|||
{
|
||||
if (tunnelSupport != null)
|
||||
return null;
|
||||
return HttpStream.super.consumeAvailable();
|
||||
return HttpStream.consumeAvailable(this, _httpChannel.getConnectionMetaData().getHttpConfiguration());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -487,6 +487,14 @@ public class HttpStreamOverHTTP3 implements HttpStream
|
|||
return committed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable consumeAvailable()
|
||||
{
|
||||
if (getTunnelSupport() != null)
|
||||
return null;
|
||||
return HttpStream.consumeAvailable(this, httpChannel.getConnectionMetaData().getHttpConfiguration());
|
||||
}
|
||||
|
||||
public boolean isIdle()
|
||||
{
|
||||
// TODO: is this necessary?
|
||||
|
|
|
@ -40,6 +40,7 @@ public class ChunksContentSource implements Content.Source
|
|||
|
||||
public ChunksContentSource(Collection<Content.Chunk> chunks)
|
||||
{
|
||||
chunks.forEach(Content.Chunk::retain);
|
||||
this.chunks = chunks;
|
||||
this.length = chunks.stream().mapToLong(c -> c.getByteBuffer().remaining()).sum();
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ public class HttpConfiguration implements Dumpable
|
|||
private boolean _relativeRedirectAllowed;
|
||||
private HostPort _serverAuthority;
|
||||
private SocketAddress _localAddress;
|
||||
private int _maxUnconsumedRequestContentReads = 16;
|
||||
|
||||
/**
|
||||
* <p>An interface that allows a request object to be customized
|
||||
|
@ -716,6 +717,28 @@ public class HttpConfiguration implements Dumpable
|
|||
_serverAuthority = authority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum amount of {@link HttpStream#read()}s that can be done by the {@link HttpStream} if the content is not
|
||||
* fully consumed by the application. If this is unable to consume to EOF then the connection will be made non-persistent.
|
||||
*
|
||||
* @param maxUnconsumedRequestContentReads the maximum amount of reads for unconsumed content or -1 for unlimited.
|
||||
*/
|
||||
public void setMaxUnconsumedRequestContentReads(int maxUnconsumedRequestContentReads)
|
||||
{
|
||||
_maxUnconsumedRequestContentReads = maxUnconsumedRequestContentReads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum amount of {@link HttpStream#read()}s that can be done by the {@link HttpStream} if the content is not
|
||||
* fully consumed by the application. If this is unable to consume to EOF then the connection will be made non-persistent.
|
||||
*
|
||||
* @return the maximum amount of reads for unconsumed content or -1 for unlimited.
|
||||
*/
|
||||
public int getMaxUnconsumedRequestContentReads()
|
||||
{
|
||||
return _maxUnconsumedRequestContentReads;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dump()
|
||||
{
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
package org.eclipse.jetty.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
|
@ -22,6 +21,7 @@ import org.eclipse.jetty.io.Connection;
|
|||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.Content.Chunk;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.StaticException;
|
||||
|
||||
/**
|
||||
* A HttpStream is an abstraction that together with {@link MetaData.Request}, represents the
|
||||
|
@ -31,6 +31,8 @@ import org.eclipse.jetty.util.Callback;
|
|||
*/
|
||||
public interface HttpStream extends Callback
|
||||
{
|
||||
Exception CONTENT_NOT_CONSUMED = new StaticException("Content not consumed");
|
||||
|
||||
/**
|
||||
* <p>Attribute name to be used as a {@link Request} attribute to store/retrieve
|
||||
* the {@link Connection} created during the HTTP/1.1 upgrade mechanism or the
|
||||
|
@ -104,27 +106,34 @@ public interface HttpStream extends Callback
|
|||
return null;
|
||||
}
|
||||
|
||||
default Throwable consumeAvailable()
|
||||
Throwable consumeAvailable();
|
||||
|
||||
static Throwable consumeAvailable(HttpStream stream, HttpConfiguration httpConfig)
|
||||
{
|
||||
while (true)
|
||||
int numReads = 0;
|
||||
int maxReads = httpConfig.getMaxUnconsumedRequestContentReads();
|
||||
while (maxReads < 0 || numReads < maxReads)
|
||||
{
|
||||
// We can always just read again here as EOF and Error content will be persistently returned.
|
||||
Content.Chunk content = read();
|
||||
Chunk content = stream.read();
|
||||
numReads++;
|
||||
|
||||
// if we cannot read to EOF then fail the stream rather than wait for unconsumed content
|
||||
if (content == null)
|
||||
return new IOException("Content not consumed");
|
||||
return CONTENT_NOT_CONSUMED;
|
||||
|
||||
// Always release any returned content. This is a noop for EOF and Error content.
|
||||
content.release();
|
||||
|
||||
// if the input failed, then fail the stream for same reason
|
||||
if (content instanceof Content.Chunk.Error error)
|
||||
if (content instanceof Chunk.Error error)
|
||||
return error.getCause();
|
||||
|
||||
if (content.isLast())
|
||||
return null;
|
||||
}
|
||||
|
||||
return CONTENT_NOT_CONSUMED;
|
||||
}
|
||||
|
||||
class Wrapper implements HttpStream
|
||||
|
|
|
@ -228,6 +228,16 @@ public interface Request extends Attributes, Content.Source
|
|||
@Override
|
||||
Content.Chunk read();
|
||||
|
||||
/**
|
||||
* Consume any available content. This bypasses any request wrappers to process the content in
|
||||
* {@link Request#read()} and reads directly from the {@link HttpStream}. This reads until
|
||||
* there is no content currently available or it reaches EOF.
|
||||
* The {@link HttpConfiguration#setMaxUnconsumedRequestContentReads(int)} configuration can be used
|
||||
* to configure how many reads will be attempted by this method.
|
||||
* @return true if the content was fully consumed.
|
||||
*/
|
||||
boolean consumeAvailable();
|
||||
|
||||
/**
|
||||
* <p>Pushes the given {@code resource} to the client.</p>
|
||||
*
|
||||
|
@ -616,6 +626,12 @@ public interface Request extends Attributes, Content.Source
|
|||
return getWrapped().read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAvailable()
|
||||
{
|
||||
return getWrapped().consumeAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
|
|
|
@ -316,13 +316,17 @@ public interface Response extends Content.Sink
|
|||
}
|
||||
|
||||
static void ensureConsumeAvailableOrNotPersistent(Request request, Response response)
|
||||
{
|
||||
if (request.consumeAvailable())
|
||||
return;
|
||||
ensureNotPersistent(request, response);
|
||||
}
|
||||
|
||||
static void ensureNotPersistent(Request request, Response response)
|
||||
{
|
||||
switch (request.getConnectionMetaData().getHttpVersion())
|
||||
{
|
||||
case HTTP_1_0:
|
||||
if (consumeAvailable(request))
|
||||
return;
|
||||
|
||||
// Remove any keep-alive value in Connection headers
|
||||
response.getHeaders().computeField(HttpHeader.CONNECTION, (h, fields) ->
|
||||
{
|
||||
|
@ -339,9 +343,6 @@ public interface Response extends Content.Sink
|
|||
break;
|
||||
|
||||
case HTTP_1_1:
|
||||
if (consumeAvailable(request))
|
||||
return;
|
||||
|
||||
// Add close value to Connection headers
|
||||
response.getHeaders().computeField(HttpHeader.CONNECTION, (h, fields) ->
|
||||
{
|
||||
|
@ -375,19 +376,6 @@ public interface Response extends Content.Sink
|
|||
}
|
||||
}
|
||||
|
||||
static boolean consumeAvailable(Request request)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = request.read();
|
||||
if (chunk == null)
|
||||
return false;
|
||||
chunk.release();
|
||||
if (chunk.isLast())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Wrapper implements Response
|
||||
{
|
||||
private final Request _request;
|
||||
|
|
|
@ -270,13 +270,13 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
super(handler, wrapped, response, callback);
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
_formData = boundary == null ? null : new MultiPartFormData(boundary);
|
||||
getRequest().setAttribute(MultiPartFormData.class.getName(), _formData);
|
||||
}
|
||||
|
||||
private void process(MultiPartFormData.Parts parts, Throwable x)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
getRequest().setAttribute(MultiPartFormData.Parts.class.getName(), parts);
|
||||
super.process();
|
||||
}
|
||||
else
|
||||
|
@ -291,7 +291,7 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
{
|
||||
// We must execute here as even though we have consumed all the input, we are probably
|
||||
// invoked in a demand runnable that is serialized with any write callbacks that might be done in process
|
||||
getRequest().getContext().execute(super::process);
|
||||
getRequest().getContext().execute(() -> process(parts, x));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -304,7 +304,7 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
{
|
||||
if (_formData == null)
|
||||
{
|
||||
super.process();
|
||||
this.process();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -313,13 +313,28 @@ public class DelayedHandler extends Handler.Wrapper
|
|||
// if we are done already, then we are still in the scope of the original process call and can
|
||||
// process directly, otherwise we must execute a call to process as we are within a serialized
|
||||
// demand callback.
|
||||
_formData.whenComplete(_formData.isDone() ? this::process : this::executeProcess);
|
||||
if (_formData.isDone())
|
||||
{
|
||||
try
|
||||
{
|
||||
MultiPartFormData.Parts parts = _formData.join();
|
||||
process(parts, null);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
process(null, t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_formData.whenComplete(this::executeProcess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readAndParse()
|
||||
{
|
||||
while (true)
|
||||
while (!_formData.isDone())
|
||||
{
|
||||
Content.Chunk chunk = getRequest().read();
|
||||
if (chunk == null)
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http.MultiPartFormData.Parts;
|
||||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.http.Trailers;
|
||||
import org.eclipse.jetty.http.UriCompliance;
|
||||
|
@ -647,6 +648,11 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
|
||||
requestLog.log(_request.getLoggedRequest(), _request._response);
|
||||
}
|
||||
|
||||
// Clean up any multipart tmp files and release any associated resources.
|
||||
Parts parts = (Parts)_request.getAttribute(Parts.class.getName());
|
||||
if (parts != null)
|
||||
parts.close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -874,6 +880,19 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAvailable()
|
||||
{
|
||||
HttpStream stream;
|
||||
try (AutoLock ignored = _lock.lock())
|
||||
{
|
||||
HttpChannelState httpChannel = lockedGetHttpChannel();
|
||||
stream = httpChannel._stream;
|
||||
}
|
||||
|
||||
return stream.consumeAvailable() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
|
|
|
@ -52,6 +52,7 @@ import org.eclipse.jetty.io.Content;
|
|||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.RetainableByteBuffer;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.io.WriteFlusher;
|
||||
import org.eclipse.jetty.io.ssl.SslConnection;
|
||||
import org.eclipse.jetty.server.ConnectionFactory;
|
||||
|
@ -594,6 +595,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
|
|||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("{} parse {}", this, _retainableByteBuffer);
|
||||
|
||||
if (_parser.isTerminated())
|
||||
throw new RuntimeIOException("Parser is terminated");
|
||||
boolean handle = _parser.parseNext(_retainableByteBuffer == null ? BufferUtil.EMPTY_BUFFER : _retainableByteBuffer.getByteBuffer());
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -1136,6 +1139,15 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
|
|||
_uri.path("/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable consumeAvailable()
|
||||
{
|
||||
Throwable result = HttpStream.consumeAvailable(this, getHttpConfiguration());
|
||||
if (result != null)
|
||||
_generator.setPersistent(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void parsedHeader(HttpField field)
|
||||
{
|
||||
HttpHeader header = field.getHeader();
|
||||
|
|
|
@ -225,6 +225,12 @@ public class MockHttpStream implements HttpStream
|
|||
return response != null && response.getStatus() >= 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable consumeAvailable()
|
||||
{
|
||||
return HttpStream.consumeAvailable(this, new HttpConfiguration());
|
||||
}
|
||||
|
||||
public boolean isComplete()
|
||||
{
|
||||
return _completed.getCount() == 0;
|
||||
|
|
|
@ -119,11 +119,11 @@ public class MultiPartByteRangesTest
|
|||
|
||||
assertEquals(3, parts.size());
|
||||
MultiPart.Part part1 = parts.get(0);
|
||||
assertEquals("12", Content.Source.asString(part1.getContent()));
|
||||
assertEquals("12", Content.Source.asString(part1.getContentSource()));
|
||||
MultiPart.Part part2 = parts.get(1);
|
||||
assertEquals("456", Content.Source.asString(part2.getContent()));
|
||||
assertEquals("456", Content.Source.asString(part2.getContentSource()));
|
||||
MultiPart.Part part3 = parts.get(2);
|
||||
assertEquals("CDEF", Content.Source.asString(part3.getContent()));
|
||||
assertEquals("CDEF", Content.Source.asString(part3.getContentSource()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ public class MultiPartFormDataHandlerTest
|
|||
.whenComplete((parts, failure) ->
|
||||
{
|
||||
if (parts != null)
|
||||
Content.copy(parts.get(0).getContent(), response, callback);
|
||||
Content.copy(parts.get(0).getContentSource(), response, callback);
|
||||
else
|
||||
Response.writeError(request, response, callback, failure);
|
||||
});
|
||||
|
@ -126,10 +126,10 @@ public class MultiPartFormDataHandlerTest
|
|||
public boolean process(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
processLatch.countDown();
|
||||
MultiPartFormData formData = (MultiPartFormData)request.getAttribute(MultiPartFormData.class.getName());
|
||||
assertNotNull(formData);
|
||||
MultiPart.Part part = formData.get().get(0);
|
||||
Content.copy(part.getContent(), response, callback);
|
||||
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
|
||||
assertNotNull(parts);
|
||||
MultiPart.Part part = parts.get(0);
|
||||
Content.copy(part.getContentSource(), response, callback);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
@ -195,8 +195,8 @@ public class MultiPartFormDataHandlerTest
|
|||
{
|
||||
if (parts != null)
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getBoundary()));
|
||||
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getBoundary());
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getMultiPartFormData().getBoundary()));
|
||||
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getMultiPartFormData().getBoundary());
|
||||
source.setPartHeadersMaxLength(1024);
|
||||
parts.forEach(source::addPart);
|
||||
source.close();
|
||||
|
@ -321,7 +321,7 @@ public class MultiPartFormDataHandlerTest
|
|||
HttpFields headers2 = part2.getHeaders();
|
||||
assertEquals(2, headers2.size());
|
||||
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals(32, part2.getContent().getLength());
|
||||
assertEquals(32, part2.getContentSource().getLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,10 +181,10 @@ public class ResourceHandlerByteRangesTest
|
|||
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()));
|
||||
assertEquals("234", Content.Source.asString(part1.getContentSource()));
|
||||
MultiPart.Part part2 = parts.get(1);
|
||||
assertEquals("text/plain", part2.getHeaders().get(HttpHeader.CONTENT_TYPE));
|
||||
assertEquals("xyz", Content.Source.asString(part2.getContent()));
|
||||
assertEquals("xyz", Content.Source.asString(part2.getContentSource()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,12 @@ public class TestableRequest implements Request
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
|
|
|
@ -286,7 +286,7 @@ public class HttpClientStreamTest extends AbstractTest
|
|||
Response response = listener.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertThrows(AsynchronousCloseException.class, stream::read);
|
||||
assertThrows(IOException.class, stream::read);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
@ -329,7 +329,7 @@ public class HttpClientStreamTest extends AbstractTest
|
|||
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
|
||||
assertThrows(AsynchronousCloseException.class, input::read);
|
||||
assertThrows(IOException.class, input::read);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
|
@ -648,6 +648,43 @@ public class BufferUtil
|
|||
}
|
||||
}
|
||||
|
||||
public static int readFrom(InputStream is, ByteBuffer buffer) throws IOException
|
||||
{
|
||||
if (buffer.hasArray())
|
||||
{
|
||||
int read = is.read(buffer.array(), buffer.arrayOffset() + buffer.limit(), buffer.capacity() - buffer.limit());
|
||||
buffer.limit(buffer.limit() + read);
|
||||
return read;
|
||||
}
|
||||
else
|
||||
{
|
||||
int totalRead = 0;
|
||||
ByteBuffer tmp = allocate(8192);
|
||||
while (BufferUtil.space(tmp) > 0 && BufferUtil.space(buffer) > 0)
|
||||
{
|
||||
int read = is.read(tmp.array(), 0, Math.min(BufferUtil.space(tmp), BufferUtil.space(buffer)));
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (read < 0)
|
||||
{
|
||||
if (totalRead == 0)
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
totalRead += read;
|
||||
tmp.position(0);
|
||||
tmp.limit(read);
|
||||
|
||||
int pos = BufferUtil.flipToFill(buffer);
|
||||
BufferUtil.put(tmp, buffer);
|
||||
BufferUtil.flipToFlush(buffer, pos);
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeTo(ByteBuffer buffer, OutputStream out) throws IOException
|
||||
{
|
||||
if (buffer.hasArray())
|
||||
|
|
|
@ -288,9 +288,11 @@ public class QueuedThreadPool extends ContainerLifeCycle implements ThreadFactor
|
|||
}
|
||||
|
||||
// Close any un-executed jobs
|
||||
while (!_jobs.isEmpty())
|
||||
while (true)
|
||||
{
|
||||
Runnable job = _jobs.poll();
|
||||
if (job == null)
|
||||
break;
|
||||
if (job instanceof Closeable)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.lang.reflect.Method;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -57,7 +58,7 @@ public class InvokerUtils
|
|||
if ((this.name != null) || (other.name != null))
|
||||
{
|
||||
// They have to match
|
||||
if (this.name.equals(other.name))
|
||||
if (Objects.equals(this.name, other.name))
|
||||
{
|
||||
if (convertible)
|
||||
{
|
||||
|
@ -79,7 +80,7 @@ public class InvokerUtils
|
|||
return false;
|
||||
}
|
||||
|
||||
// Not named, then its a simple type / assignable match
|
||||
// Not named, then it's a simple type / assignable match
|
||||
return (other.type.isAssignableFrom(this.type));
|
||||
}
|
||||
|
||||
|
@ -112,11 +113,6 @@ public class InvokerUtils
|
|||
{
|
||||
return required;
|
||||
}
|
||||
|
||||
public boolean isConvertible()
|
||||
{
|
||||
return convertible;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ParamIdentifier
|
||||
|
@ -218,7 +214,7 @@ public class InvokerUtils
|
|||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
|
||||
// Construct Actual Calling Args.
|
||||
// This is the array of args, arriving as all of the named variables (usually static in nature),
|
||||
// This is the array of args, arriving as all the named variables (usually static in nature),
|
||||
// then the raw calling arguments (very dynamic in nature)
|
||||
Arg[] callingArgs = new Arg[rawCallingArgs.length + (namedVariables == null ? 0 : namedVariables.length)];
|
||||
{
|
||||
|
@ -253,13 +249,11 @@ public class InvokerUtils
|
|||
}
|
||||
|
||||
// Parameter to Calling Argument mapping.
|
||||
// The size of this array must be the the same as the parameterArgs array (or bigger)
|
||||
// The size of this array must be the same as the parameterArgs array (or bigger)
|
||||
if (callingArgs.length < parameterTypes.length)
|
||||
{
|
||||
if (!throwOnFailure)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Target method ");
|
||||
|
@ -275,17 +269,12 @@ public class InvokerUtils
|
|||
List<Class<?>> cTypes = new ArrayList<>();
|
||||
{
|
||||
cTypes.add(targetClass); // targetClass always at index 0
|
||||
for (int i = 0; i < callingArgs.length; i++)
|
||||
for (Arg arg : callingArgs)
|
||||
{
|
||||
Arg arg = callingArgs[i];
|
||||
if (arg.name != null)
|
||||
{
|
||||
hasNamedCallingArgs = true;
|
||||
}
|
||||
if (arg.convertible)
|
||||
{
|
||||
hasConvertibleTypes = true;
|
||||
}
|
||||
cTypes.add(arg.getType());
|
||||
}
|
||||
}
|
||||
|
@ -304,9 +293,7 @@ public class InvokerUtils
|
|||
// If callingType and rawType are the same (and there's no named args),
|
||||
// then there's no need to reorder / permute / drop args
|
||||
if (!hasNamedCallingArgs && !hasNamedParamArgs && rawType.equals(callingType))
|
||||
{
|
||||
return methodHandle;
|
||||
}
|
||||
|
||||
// If we reached this point, then we know that the callingType and rawType don't
|
||||
// match, so we have to drop and/or permute(reorder) the arguments
|
||||
|
@ -341,9 +328,7 @@ public class InvokerUtils
|
|||
if (ref < 0)
|
||||
{
|
||||
if (!throwOnFailure)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Invalid mapping of type [");
|
||||
|
@ -364,14 +349,12 @@ public class InvokerUtils
|
|||
{
|
||||
for (int uci = 0; uci < usedCallingArgs.length; uci++)
|
||||
{
|
||||
if (usedCallingArgs[uci] == false)
|
||||
if (!usedCallingArgs[uci])
|
||||
{
|
||||
if (callingArgs[uci].required)
|
||||
{
|
||||
if (!throwOnFailure)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Missing required argument [");
|
||||
|
@ -407,9 +390,8 @@ public class InvokerUtils
|
|||
// Use converted Types for callingArgs
|
||||
cTypes = new ArrayList<>();
|
||||
cTypes.add(targetClass); // targetClass always at index 0
|
||||
for (int i = 0; i < callingArgs.length; i++)
|
||||
for (Arg arg : callingArgs)
|
||||
{
|
||||
Arg arg = callingArgs[i];
|
||||
cTypes.add(arg.getConvertedType());
|
||||
}
|
||||
callingType = MethodType.methodType(method.getReturnType(), cTypes);
|
||||
|
@ -478,18 +460,4 @@ public class InvokerUtils
|
|||
}
|
||||
str.append(")");
|
||||
}
|
||||
|
||||
private static void appendTypeList(StringBuilder str, Class<?>[] types)
|
||||
{
|
||||
str.append("(");
|
||||
boolean comma = false;
|
||||
for (Class<?> type : types)
|
||||
{
|
||||
if (comma)
|
||||
str.append(", ");
|
||||
str.append(type.getName());
|
||||
comma = true;
|
||||
}
|
||||
str.append(")");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,10 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.websocket.core.exception.DuplicateAnnotationException;
|
||||
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
|
||||
|
||||
public class ReflectUtils
|
||||
{
|
||||
|
||||
private static final Pattern JAVAX_CLASSNAME_PATTERN = Pattern.compile("^javax*\\..*");
|
||||
private static final Pattern JAKARTA_CLASSNAME_PATTERN = Pattern.compile("^jakarta*\\..*");
|
||||
|
||||
private static class GenericRef
|
||||
|
@ -61,37 +60,27 @@ public class ReflectUtils
|
|||
|
||||
public void setGenericFromType(Type type, int index)
|
||||
{
|
||||
// debug("setGenericFromType(%s,%d)",toShortName(type),index);
|
||||
this.genericType = type;
|
||||
this.genericIndex = index;
|
||||
if (type instanceof Class)
|
||||
{
|
||||
this.genericClass = (Class<?>)type;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("GenericRef [baseClass=");
|
||||
builder.append(baseClass);
|
||||
builder.append(", ifaceClass=");
|
||||
builder.append(ifaceClass);
|
||||
builder.append(", genericType=");
|
||||
builder.append(genericType);
|
||||
builder.append(", genericClass=");
|
||||
builder.append(genericClass);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
return "GenericRef [baseClass=" + baseClass +
|
||||
", ifaceClass=" + ifaceClass +
|
||||
", genericType=" + genericType +
|
||||
", genericClass=" + genericClass +
|
||||
"]";
|
||||
}
|
||||
}
|
||||
|
||||
private static StringBuilder appendTypeName(StringBuilder sb, Type type, boolean ellipses)
|
||||
{
|
||||
if (type instanceof Class<?>)
|
||||
if (type instanceof Class<?> ctype)
|
||||
{
|
||||
Class<?> ctype = (Class<?>)type;
|
||||
if (ctype.isArray())
|
||||
{
|
||||
try
|
||||
|
@ -106,13 +95,9 @@ public class ReflectUtils
|
|||
for (int i = 0; i < dimensions; i++)
|
||||
{
|
||||
if (ellipses)
|
||||
{
|
||||
sb.append("...");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("[]");
|
||||
}
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
@ -132,63 +117,6 @@ public class ReflectUtils
|
|||
return sb;
|
||||
}
|
||||
|
||||
public static void assertIsAnnotated(Method method, Class<? extends Annotation> annoClass)
|
||||
{
|
||||
if (method.getAnnotation(annoClass) == null)
|
||||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Method does not declare required @");
|
||||
err.append(annoClass.getName());
|
||||
err.append(" annotation: ");
|
||||
err.append(method);
|
||||
|
||||
throw new InvalidWebSocketException(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertIsPublicNonStatic(Method method)
|
||||
{
|
||||
int mods = method.getModifiers();
|
||||
if (!Modifier.isPublic(mods))
|
||||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Invalid declaration of ");
|
||||
err.append(method);
|
||||
err.append(System.lineSeparator());
|
||||
|
||||
err.append("Method modifier must be public");
|
||||
|
||||
throw new InvalidWebSocketException(err.toString());
|
||||
}
|
||||
|
||||
if (Modifier.isStatic(mods))
|
||||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Invalid declaration of ");
|
||||
err.append(method);
|
||||
err.append(System.lineSeparator());
|
||||
|
||||
err.append("Method modifier must not be static");
|
||||
|
||||
throw new InvalidWebSocketException(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertIsReturn(Method method, Class<?> type)
|
||||
{
|
||||
if (!type.equals(method.getReturnType()))
|
||||
{
|
||||
StringBuilder err = new StringBuilder();
|
||||
err.append("Invalid declaration of ");
|
||||
err.append(method);
|
||||
err.append(System.lineSeparator());
|
||||
|
||||
err.append("Return type must be ").append(type);
|
||||
|
||||
throw new InvalidWebSocketException(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static Method findMethod(Class<?> pojo, String methodName, Class<?>... params)
|
||||
{
|
||||
try
|
||||
|
@ -205,15 +133,9 @@ public class ReflectUtils
|
|||
{
|
||||
Method[] methods = findAnnotatedMethods(pojo, anno);
|
||||
if (methods == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (methods.length > 1)
|
||||
{
|
||||
throw DuplicateAnnotationException.build(pojo, anno, methods);
|
||||
}
|
||||
|
||||
return methods[0];
|
||||
}
|
||||
|
||||
|
@ -245,28 +167,20 @@ public class ReflectUtils
|
|||
{
|
||||
GenericRef ref = new GenericRef(baseClass, ifaceClass);
|
||||
if (resolveGenericRef(ref, baseClass))
|
||||
{
|
||||
// debug("Generic Found: %s",ref.genericClass);
|
||||
return ref.genericClass;
|
||||
}
|
||||
|
||||
// debug("Generic not found: %s",ref);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int findTypeParameterIndex(Class<?> clazz, TypeVariable<?> needVar)
|
||||
{
|
||||
// debug("findTypeParameterIndex(%s, [%s])",toShortName(clazz),toShortName(needVar));
|
||||
TypeVariable<?>[] params = clazz.getTypeParameters();
|
||||
for (int i = 0; i < params.length; i++)
|
||||
{
|
||||
if (params[i].getName().equals(needVar.getName()))
|
||||
{
|
||||
// debug("Type Parameter found at index: [%d]",i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// debug("Type Parameter NOT found");
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -293,26 +207,6 @@ public class ReflectUtils
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean isSameParameters(Class<?>[] actual, Class<?>[] params)
|
||||
{
|
||||
if (actual.length != params.length)
|
||||
{
|
||||
// skip
|
||||
return false;
|
||||
}
|
||||
|
||||
int len = params.length;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (!actual[i].equals(params[i]))
|
||||
{
|
||||
return false; // not valid
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean resolveGenericRef(GenericRef ref, Class<?> clazz, Type type)
|
||||
{
|
||||
if (type instanceof Class)
|
||||
|
@ -320,7 +214,6 @@ public class ReflectUtils
|
|||
if (type == ref.ifaceClass)
|
||||
{
|
||||
// is this a straight ref or a TypeVariable?
|
||||
// debug("Found ref (as class): %s",toShortName(type));
|
||||
ref.setGenericFromType(type, 0);
|
||||
return true;
|
||||
}
|
||||
|
@ -331,13 +224,11 @@ public class ReflectUtils
|
|||
}
|
||||
}
|
||||
|
||||
if (type instanceof ParameterizedType)
|
||||
if (type instanceof ParameterizedType ptype)
|
||||
{
|
||||
ParameterizedType ptype = (ParameterizedType)type;
|
||||
Type rawType = ptype.getRawType();
|
||||
if (rawType == ref.ifaceClass)
|
||||
{
|
||||
// debug("Found ref on [%s] as ParameterizedType [%s]",toShortName(clazz),toShortName(ptype));
|
||||
// Always get the raw type parameter, let unwrap() solve for what it is
|
||||
ref.setGenericFromType(ptype.getActualTypeArguments()[0], 0);
|
||||
return true;
|
||||
|
@ -354,45 +245,31 @@ public class ReflectUtils
|
|||
private static boolean resolveGenericRef(GenericRef ref, Type type)
|
||||
{
|
||||
if ((type == null) || (type == Object.class))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type instanceof Class)
|
||||
if (type instanceof Class<?> clazz)
|
||||
{
|
||||
Class<?> clazz = (Class<?>)type;
|
||||
// prevent spinning off into Serialization and other parts of the
|
||||
// standard tree that we could care less about
|
||||
if (JAKARTA_CLASSNAME_PATTERN.matcher(clazz.getName()).matches())
|
||||
{
|
||||
// Prevent spinning off into Serialization and other parts of the standard tree that we couldn't care less about.
|
||||
if (JAKARTA_CLASSNAME_PATTERN.matcher(clazz.getName()).matches() || JAVAX_CLASSNAME_PATTERN.matcher(clazz.getName()).matches())
|
||||
return false;
|
||||
}
|
||||
|
||||
Type[] ifaces = clazz.getGenericInterfaces();
|
||||
for (Type iface : ifaces)
|
||||
{
|
||||
// debug("resolve %s interface[]: %s",toShortName(clazz),toShortName(iface));
|
||||
if (resolveGenericRef(ref, clazz, iface))
|
||||
{
|
||||
if (ref.needsUnwrap())
|
||||
{
|
||||
// debug("## Unwrap class %s::%s",toShortName(clazz),toShortName(iface));
|
||||
TypeVariable<?> needVar = (TypeVariable<?>)ref.genericType;
|
||||
// debug("needs unwrap of type var [%s] - index [%d]",toShortName(needVar),ref.genericIndex);
|
||||
|
||||
// attempt to find typeParameter on class itself
|
||||
int typeParamIdx = findTypeParameterIndex(clazz, needVar);
|
||||
// debug("type param index for %s[%s] is [%d]",toShortName(clazz),toShortName(needVar),typeParamIdx);
|
||||
|
||||
if (typeParamIdx >= 0)
|
||||
{
|
||||
// found a type parameter, use it
|
||||
// debug("unwrap from class [%s] - typeParameters[%d]",toShortName(clazz),typeParamIdx);
|
||||
TypeVariable<?>[] params = clazz.getTypeParameters();
|
||||
if (params.length >= typeParamIdx)
|
||||
{
|
||||
ref.setGenericFromType(params[typeParamIdx], typeParamIdx);
|
||||
}
|
||||
}
|
||||
else if (iface instanceof ParameterizedType)
|
||||
{
|
||||
|
@ -409,19 +286,15 @@ public class ReflectUtils
|
|||
return resolveGenericRef(ref, type);
|
||||
}
|
||||
|
||||
if (type instanceof ParameterizedType)
|
||||
if (type instanceof ParameterizedType ptype)
|
||||
{
|
||||
ParameterizedType ptype = (ParameterizedType)type;
|
||||
Class<?> rawClass = (Class<?>)ptype.getRawType();
|
||||
if (resolveGenericRef(ref, rawClass))
|
||||
{
|
||||
if (ref.needsUnwrap())
|
||||
{
|
||||
// debug("## Unwrap ParameterizedType %s::%s",toShortName(type),toShortName(rawClass));
|
||||
TypeVariable<?> needVar = (TypeVariable<?>)ref.genericType;
|
||||
// debug("needs unwrap of type var [%s] - index [%d]",toShortName(needVar),ref.genericIndex);
|
||||
int typeParamIdx = findTypeParameterIndex(rawClass, needVar);
|
||||
// debug("type paramIdx of %s::%s is index [%d]",toShortName(rawClass),toShortName(needVar),typeParamIdx);
|
||||
|
||||
Type arg = ptype.getActualTypeArguments()[typeParamIdx];
|
||||
ref.setGenericFromType(arg, typeParamIdx);
|
||||
|
@ -433,70 +306,19 @@ public class ReflectUtils
|
|||
return false;
|
||||
}
|
||||
|
||||
public static String toShortName(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
return "<null>";
|
||||
}
|
||||
|
||||
if (type instanceof Class)
|
||||
{
|
||||
String name = ((Class<?>)type).getName();
|
||||
return trimClassName(name);
|
||||
}
|
||||
|
||||
if (type instanceof ParameterizedType)
|
||||
{
|
||||
ParameterizedType ptype = (ParameterizedType)type;
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append(trimClassName(((Class<?>)ptype.getRawType()).getName()));
|
||||
str.append("<");
|
||||
Type[] args = ptype.getActualTypeArguments();
|
||||
for (int i = 0; i < args.length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
str.append(",");
|
||||
}
|
||||
str.append(args[i]);
|
||||
}
|
||||
str.append(">");
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
return type.toString();
|
||||
}
|
||||
|
||||
public static String toString(Class<?> pojo, Method method)
|
||||
{
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
append(str, pojo, method);
|
||||
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static String trimClassName(String name)
|
||||
{
|
||||
int idx = name.lastIndexOf('.');
|
||||
name = name.substring(idx + 1);
|
||||
idx = name.lastIndexOf('$');
|
||||
if (idx >= 0)
|
||||
{
|
||||
name = name.substring(idx + 1);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public static void append(StringBuilder str, Class<?> pojo, Method method)
|
||||
{
|
||||
// method modifiers
|
||||
int mod = method.getModifiers() & Modifier.methodModifiers();
|
||||
if (mod != 0)
|
||||
{
|
||||
str.append(Modifier.toString(mod)).append(' ');
|
||||
}
|
||||
|
||||
// return type
|
||||
Type retType = method.getGenericReturnType();
|
||||
|
@ -525,8 +347,6 @@ public class ReflectUtils
|
|||
}
|
||||
}
|
||||
str.append(')');
|
||||
|
||||
// TODO: show exceptions?
|
||||
}
|
||||
|
||||
public static void append(StringBuilder str, Method method)
|
||||
|
|
|
@ -83,7 +83,7 @@ public final class TextUtils
|
|||
|
||||
StringBuilder ret = new StringBuilder();
|
||||
int startLen = (int)Math.round((double)max / (double)3);
|
||||
ret.append(raw.substring(0, startLen));
|
||||
ret.append(raw, 0, startLen);
|
||||
ret.append("...");
|
||||
ret.append(raw.substring(length - (max - startLen - 3)));
|
||||
|
||||
|
|
|
@ -78,10 +78,6 @@
|
|||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.el</groupId>
|
||||
<artifactId>jakarta.el-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-http-tools</artifactId>
|
||||
|
|
|
@ -39,13 +39,8 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet.jsp</groupId>
|
||||
<artifactId>jakarta.servlet.jsp-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.el</groupId>
|
||||
<artifactId>jakarta.el-api</artifactId>
|
||||
<groupId>org.eclipse.jetty.ee10</groupId>
|
||||
<artifactId>jetty-ee10-apache-jsp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@ -54,12 +49,6 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee10</groupId>
|
||||
<artifactId>jetty-ee10-apache-jsp</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee10</groupId>
|
||||
<artifactId>jetty-ee10-annotations</artifactId>
|
||||
|
|
|
@ -6,5 +6,8 @@ Enables the glassfish version of JSTL for all webapps.
|
|||
[environment]
|
||||
ee10
|
||||
|
||||
[depends]
|
||||
ee10-apache-jsp
|
||||
|
||||
[lib]
|
||||
lib/ee10-glassfish-jstl/*.jar
|
||||
|
|
|
@ -240,8 +240,8 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<prependGroupId>true</prependGroupId>
|
||||
<includeGroupIds>org.mortbay.jasper,org.eclipse.jdt</includeGroupIds>
|
||||
<includeArtifactIds>apache-el,apache-jsp,ecj</includeArtifactIds>
|
||||
<includeGroupIds>jakarta.servlet.jsp,jakart.el,org.mortbay.jasper,org.eclipse.jdt</includeGroupIds>
|
||||
<includeArtifactIds>jakart.servlet.jsp-api,jakarta.el-api,apache-el,apache-jsp,ecj</includeArtifactIds>
|
||||
<includeTypes>jar</includeTypes>
|
||||
<classifier>sources</classifier>
|
||||
<outputDirectory>${source-assembly-directory}/lib/ee10-apache-jsp</outputDirectory>
|
||||
|
@ -255,8 +255,8 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<prependGroupId>true</prependGroupId>
|
||||
<includeGroupIds>org.mortbay.jasper,org.eclipse.jdt</includeGroupIds>
|
||||
<includeArtifactIds>apache-el,apache-jsp,ecj</includeArtifactIds>
|
||||
<includeGroupIds>jakarta.servlet.jsp,jakarta.el,org.mortbay.jasper,org.eclipse.jdt</includeGroupIds>
|
||||
<includeArtifactIds>jakarta.servlet.jsp-api,jakarta.el-api,apache-el,apache-jsp,ecj</includeArtifactIds>
|
||||
<includeTypes>jar</includeTypes>
|
||||
<outputDirectory>${assembly-directory}/lib/ee10-apache-jsp</outputDirectory>
|
||||
</configuration>
|
||||
|
@ -269,8 +269,8 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<prependGroupId>true</prependGroupId>
|
||||
<includeGroupIds>jakarta.servlet.jsp.jstl,org.glassfish.web,jakarta.el,jakarta.servlet.jsp,jakarta.el</includeGroupIds>
|
||||
<includeArtifactIds>jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl,jakarta.el-api,jakarta.servlet.jsp-api,jakarta.el-api</includeArtifactIds>
|
||||
<includeGroupIds>jakarta.servlet.jsp.jstl,org.glassfish.web</includeGroupIds>
|
||||
<includeArtifactIds>jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl</includeArtifactIds>
|
||||
<includeTypes>jar</includeTypes>
|
||||
<classifier>sources</classifier>
|
||||
<outputDirectory>${source-assembly-directory}/lib/ee10-glassfish-jstl</outputDirectory>
|
||||
|
@ -284,8 +284,8 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<prependGroupId>true</prependGroupId>
|
||||
<includeGroupIds>jakarta.servlet.jsp.jstl,org.glassfish.web,jakarta.el,jakarta.servlet.jsp,jakarta.el</includeGroupIds>
|
||||
<includeArtifactIds>jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl,jakarta.el-api,jakarta.servlet.jsp-api,jakarta.el-api</includeArtifactIds>
|
||||
<includeGroupIds>jakarta.servlet.jsp.jstl,org.glassfish.web</includeGroupIds>
|
||||
<includeArtifactIds>jakarta.servlet.jsp.jstl-api,jakarta.servlet.jsp.jstl</includeArtifactIds>
|
||||
<includeTypes>jar</includeTypes>
|
||||
<outputDirectory>${assembly-directory}/lib/ee10-glassfish-jstl</outputDirectory>
|
||||
</configuration>
|
||||
|
|
|
@ -167,6 +167,8 @@ public class TestOSGiUtil
|
|||
res.add(CoreOptions.streamBundle(loggingPropertiesBundle.build()).noStart());
|
||||
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-slf4j-impl").versionAsInProject().start());
|
||||
// END - slf4j 2.x
|
||||
|
||||
res.add(mavenBundle().groupId("jakarta.el").artifactId("jakarta.el-api").versionAsInProject().start());
|
||||
|
||||
res.add(mavenBundle().groupId("jakarta.servlet").artifactId("jakarta.servlet-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("org.eclipse.platform").artifactId("org.eclipse.osgi.util").versionAsInProject());
|
||||
|
@ -195,11 +197,10 @@ public class TestOSGiUtil
|
|||
res.add(mavenBundle().groupId("org.apache.aries.spifly").artifactId("org.apache.aries.spifly.dynamic.bundle").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.inject").artifactId("jakarta.inject-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.annotation").artifactId("jakarta.annotation-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.enterprise").artifactId("jakarta.enterprise.cdi-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.enterprise").artifactId("jakarta.enterprise.lang-model").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.interceptor").artifactId("jakarta.interceptor-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.enterprise").artifactId("jakarta.enterprise.lang-model").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.enterprise").artifactId("jakarta.enterprise.cdi-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.transaction").artifactId("jakarta.transaction-api").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("jakarta.el").artifactId("jakarta.el-api").versionAsInProject().start());
|
||||
|
||||
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-util").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-io").versionAsInProject().start());
|
||||
|
@ -237,26 +238,8 @@ public class TestOSGiUtil
|
|||
public static void coreJspDependencies(List<Option> res)
|
||||
{
|
||||
//jetty jsp bundles
|
||||
|
||||
/* The coreJettyDependencies() method needs to configure jakarta.el-api to satisfy the jakarta.transaction-api bundle.
|
||||
* However, as we are now configuring the full jsp bundle set, we need to remove the jakarta.el-api
|
||||
* bundle because the org.mortbay.jasper.apache-el bundle will be providing both the api and the impl.
|
||||
*/
|
||||
MavenArtifactProvisionOption option = mavenBundle().groupId("jakarta.el").artifactId("jakarta.el-api").versionAsInProject();
|
||||
|
||||
ListIterator<Option> iter = res.listIterator();
|
||||
while (iter.hasNext())
|
||||
{
|
||||
Option o = iter.next();
|
||||
if (o instanceof MavenArtifactProvisionOption)
|
||||
{
|
||||
if (((MavenArtifactProvisionOption)o).getURL().contains("jakarta.el-api"))
|
||||
{
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.add(systemProperty("jakarta.el.ExpressionFactory").value("org.apache.el.ExpressionFactoryImpl"));
|
||||
res.add(mavenBundle().groupId("jakarta.servlet.jsp").artifactId("jakarta.servlet.jsp-api").versionAsInProject());
|
||||
res.add(mavenBundle().groupId("org.mortbay.jasper").artifactId("apache-el").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("org.mortbay.jasper").artifactId("apache-jsp").versionAsInProject().start());
|
||||
res.add(mavenBundle().groupId("org.eclipse.jetty.ee10").artifactId("jetty-ee10-apache-jsp").versionAsInProject().start());
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
package org.eclipse.jetty.ee10.proxy;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
|
@ -1360,7 +1359,7 @@ public class ProxyServletTest
|
|||
|
||||
chunk1Latch.countDown();
|
||||
|
||||
assertThrows(EOFException.class, () ->
|
||||
assertThrows(IOException.class, () ->
|
||||
{
|
||||
// Make sure the proxy does not receive chunk2.
|
||||
input.read();
|
||||
|
|
|
@ -60,11 +60,9 @@ class AsyncContentProducer implements ContentProducer
|
|||
|
||||
// Make sure that asking this instance for chunks between
|
||||
// recycle() and reopen() will only produce error chunks.
|
||||
if (_chunk == null)
|
||||
_chunk = RECYCLED_ERROR_CHUNK;
|
||||
// The chunk must be fully consumed.
|
||||
else if (!_chunk.isLast() || _chunk.hasRemaining())
|
||||
throw new IllegalStateException("ContentProducer with unconsumed chunk cannot be recycled");
|
||||
if (_chunk != null)
|
||||
_chunk.release();
|
||||
_chunk = RECYCLED_ERROR_CHUNK;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -182,18 +180,7 @@ class AsyncContentProducer implements ContentProducer
|
|||
|
||||
private boolean consumeAvailableChunks()
|
||||
{
|
||||
ServletContextRequest request = _servletChannel.getServletContextRequest();
|
||||
while (true)
|
||||
{
|
||||
Content.Chunk chunk = request.read();
|
||||
if (chunk == null)
|
||||
return false;
|
||||
|
||||
chunk.release();
|
||||
|
||||
if (chunk.isLast())
|
||||
return true;
|
||||
}
|
||||
return _servletChannel.getServletContextRequest().consumeAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,32 +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.ee10.servlet;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.Part;
|
||||
|
||||
public class MultiPartFormInputStream
|
||||
{
|
||||
public MultiPartFormInputStream(InputStream inputStream, String contentType, Object o, Object o1)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public List<Part> getParts()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -15,9 +15,11 @@ package org.eclipse.jetty.ee10.servlet;
|
|||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.charset.UnsupportedCharsetException;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
|
@ -35,6 +37,7 @@ import java.util.concurrent.ExecutionException;
|
|||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletConnection;
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
@ -65,6 +68,7 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.server.ConnectionMetaData;
|
||||
import org.eclipse.jetty.server.FormFields;
|
||||
import org.eclipse.jetty.server.HttpCookieUtils;
|
||||
|
@ -74,6 +78,7 @@ import org.eclipse.jetty.session.AbstractSessionManager;
|
|||
import org.eclipse.jetty.session.ManagedSession;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.HostPort;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -473,11 +478,76 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
@Override
|
||||
public Collection<Part> getParts() throws IOException, ServletException
|
||||
{
|
||||
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 (_parts == null)
|
||||
_parts = ServletMultiPartFormData.from(this);
|
||||
{
|
||||
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()));
|
||||
|
||||
MultipartConfigElement config = (MultipartConfigElement)getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
|
||||
if (config == null)
|
||||
throw new IllegalStateException("No multipart config for servlet");
|
||||
|
||||
ServletContextHandler contextHandler = _request.getContext().getServletContextHandler();
|
||||
int maxFormContentSize = contextHandler.getMaxFormContentSize();
|
||||
int maxFormKeys = contextHandler.getMaxFormKeys();
|
||||
|
||||
_parts = ServletMultiPartFormData.from(this, maxFormKeys);
|
||||
Collection<Part> parts = _parts.getParts();
|
||||
|
||||
String formCharset = null;
|
||||
Part charsetPart = _parts.getPart("_charset_");
|
||||
if (charsetPart != null)
|
||||
{
|
||||
try (InputStream is = charsetPart.getInputStream())
|
||||
{
|
||||
formCharset = IO.toString(is, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
|
||||
1. Use the part specific charset as provided in that part's Content-Type header; else
|
||||
2. Use the overall default charset. Determined by:
|
||||
a. if part name _charset_ exists, use that part's value.
|
||||
b. if the request.getCharacterEncoding() returns a value, use that.
|
||||
(note, this can be either from the charset field on the request Content-Type
|
||||
header, or from a manual call to request.setCharacterEncoding())
|
||||
c. use utf-8.
|
||||
*/
|
||||
Charset defaultCharset;
|
||||
if (formCharset != null)
|
||||
defaultCharset = Charset.forName(formCharset);
|
||||
else if (getCharacterEncoding() != null)
|
||||
defaultCharset = Charset.forName(getCharacterEncoding());
|
||||
else
|
||||
defaultCharset = StandardCharsets.UTF_8;
|
||||
|
||||
long formContentSize = 0;
|
||||
for (Part p : parts)
|
||||
{
|
||||
if (p.getSubmittedFileName() == null)
|
||||
{
|
||||
formContentSize = Math.addExact(formContentSize, p.getSize());
|
||||
if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
|
||||
throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
|
||||
|
||||
// Servlet Spec 3.0 pg 23, parts without filename must be put into params.
|
||||
String charset = null;
|
||||
if (p.getContentType() != null)
|
||||
charset = MimeTypes.getCharsetFromContentType(p.getContentType());
|
||||
|
||||
try (InputStream is = p.getInputStream())
|
||||
{
|
||||
String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
|
||||
if (_contentParameters == null)
|
||||
_contentParameters = new Fields();
|
||||
_contentParameters.add(p.getName(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _parts.getParts();
|
||||
}
|
||||
|
||||
|
@ -780,13 +850,47 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
{
|
||||
try
|
||||
{
|
||||
int maxKeys = _request.getServletRequestState().getContextHandler().getMaxFormKeys();
|
||||
int maxContentSize = _request.getServletRequestState().getContextHandler().getMaxFormContentSize();
|
||||
_contentParameters = FormFields.from(getServletContextRequest(), maxKeys, maxContentSize).get();
|
||||
int contentLength = getContentLength();
|
||||
if (contentLength != 0 && _inputState == ServletContextRequest.INPUT_NONE)
|
||||
{
|
||||
String baseType = HttpField.valueParameters(getContentType(), null);
|
||||
if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
|
||||
_request.getConnectionMetaData().getHttpConfiguration().isFormEncodedMethod(getMethod()))
|
||||
{
|
||||
try
|
||||
{
|
||||
int maxKeys = _request.getServletRequestState().getContextHandler().getMaxFormKeys();
|
||||
int maxContentSize = _request.getServletRequestState().getContextHandler().getMaxFormContentSize();
|
||||
_contentParameters = FormFields.from(getServletContextRequest(), maxKeys, maxContentSize).get();
|
||||
}
|
||||
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
|
||||
InterruptedException e)
|
||||
{
|
||||
LOG.warn(e.toString());
|
||||
throw new BadMessageException("Unable to parse form content", e);
|
||||
}
|
||||
}
|
||||
else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
|
||||
getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT) != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
getParts();
|
||||
}
|
||||
catch (IOException | ServletException e)
|
||||
{
|
||||
String msg = "Unable to extract content parameters";
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(msg, e);
|
||||
throw new RuntimeIOException(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_contentParameters == null || _contentParameters.isEmpty())
|
||||
_contentParameters = ServletContextRequest.NO_PARAMS;
|
||||
}
|
||||
catch (IllegalStateException | IllegalArgumentException | ExecutionException | InterruptedException e)
|
||||
catch (IllegalStateException | IllegalArgumentException e)
|
||||
{
|
||||
LOG.warn(e.toString());
|
||||
throw new BadMessageException("Unable to parse form content", e);
|
||||
|
|
|
@ -489,7 +489,8 @@ public class ServletChannel
|
|||
// from the failed dispatch, then we try to consume it here and if we fail we add a
|
||||
// Connection:close. This can't be deferred to COMPLETE as the response will be committed
|
||||
// by then.
|
||||
Response.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getResponse());
|
||||
if (!_httpInput.consumeAvailable())
|
||||
Response.ensureNotPersistent(_servletContextRequest, _servletContextRequest.getResponse());
|
||||
|
||||
ContextHandler.ScopedContext context = (ContextHandler.ScopedContext)_servletContextRequest.getAttribute(ErrorHandler.ERROR_CONTEXT);
|
||||
Request.Processor errorProcessor = ErrorHandler.getErrorProcessor(getServer(), context == null ? null : context.getContextHandler());
|
||||
|
|
|
@ -44,7 +44,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
public class ServletContextRequest extends ContextRequest
|
||||
{
|
||||
public static final String __MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
|
||||
public static final String MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ServletContextRequest.class);
|
||||
static final int INPUT_NONE = 0;
|
||||
static final int INPUT_STREAM = 1;
|
||||
|
|
|
@ -715,7 +715,7 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
|
|||
{
|
||||
MultipartConfigElement mpce = ((Registration)_registration).getMultipartConfig();
|
||||
if (mpce != null)
|
||||
request.setAttribute(ServletContextRequest.__MULTIPART_CONFIG_ELEMENT, mpce);
|
||||
request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, mpce);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,9 +29,12 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
import org.eclipse.jetty.http.MultiPart;
|
||||
import org.eclipse.jetty.http.MultiPartFormData;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.RetainableByteBuffer;
|
||||
import org.eclipse.jetty.server.ConnectionMetaData;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
||||
|
@ -56,16 +59,31 @@ public class ServletMultiPartFormData
|
|||
* @see org.eclipse.jetty.server.handler.DelayedHandler
|
||||
*/
|
||||
public static Parts from(ServletApiRequest request) throws IOException
|
||||
{
|
||||
return from(request, ServletContextHandler.DEFAULT_MAX_FORM_KEYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Parses the request content assuming it is a multipart content,
|
||||
* and returns a {@link Parts} objects that can be used to access
|
||||
* individual {@link Part}s.</p>
|
||||
*
|
||||
* @param request the HTTP request with multipart content
|
||||
* @return a {@link Parts} object to access the individual {@link Part}s
|
||||
* @throws IOException if reading the request content fails
|
||||
* @see org.eclipse.jetty.server.handler.DelayedHandler
|
||||
*/
|
||||
public static Parts from(ServletApiRequest request, int maxParts) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
// Look for a previously read and parsed MultiPartFormData from the DelayedHandler
|
||||
MultiPartFormData formData = (MultiPartFormData)request.getAttribute(MultiPartFormData.class.getName());
|
||||
if (formData != null)
|
||||
return new Parts(formData);
|
||||
// Look for a previously read and parsed MultiPartFormData from the DelayedHandler.
|
||||
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
|
||||
if (parts != null)
|
||||
return new Parts(parts);
|
||||
|
||||
// TODO set the files directory
|
||||
return new ServletMultiPartFormData().parse(request);
|
||||
return new ServletMultiPartFormData().parse(request, maxParts);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
|
@ -73,9 +91,9 @@ public class ServletMultiPartFormData
|
|||
}
|
||||
}
|
||||
|
||||
private Parts parse(ServletApiRequest request) throws IOException
|
||||
private Parts parse(ServletApiRequest request, int maxParts) throws IOException
|
||||
{
|
||||
MultipartConfigElement config = (MultipartConfigElement)request.getAttribute(ServletContextRequest.__MULTIPART_CONFIG_ELEMENT);
|
||||
MultipartConfigElement config = (MultipartConfigElement)request.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
|
||||
if (config == null)
|
||||
throw new IllegalStateException("No multipart configuration element");
|
||||
|
||||
|
@ -83,7 +101,9 @@ public class ServletMultiPartFormData
|
|||
if (boundary == null)
|
||||
throw new IllegalStateException("No multipart boundary parameter in Content-Type");
|
||||
|
||||
// Store MultiPartFormData as attribute on request so it is released by the HttpChannel.
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
formData.setMaxParts(maxParts);
|
||||
|
||||
File tmpDirFile = (File)request.getServletContext().getAttribute(ServletContext.TEMPDIR);
|
||||
if (tmpDirFile == null)
|
||||
|
@ -99,24 +119,36 @@ public class ServletMultiPartFormData
|
|||
ConnectionMetaData connectionMetaData = request.getServletContextRequest().getConnectionMetaData();
|
||||
formData.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
|
||||
|
||||
ByteBufferPool byteBufferPool = request.getServletContextRequest().getComponents().getByteBufferPool();
|
||||
Connection connection = connectionMetaData.getConnection();
|
||||
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
InputStream input = request.getInputStream();
|
||||
while (true)
|
||||
while (!formData.isDone())
|
||||
{
|
||||
int read = input.read(buffer);
|
||||
if (read < 0)
|
||||
RetainableByteBuffer retainable = byteBufferPool.acquire(bufferSize, false);
|
||||
boolean readEof = false;
|
||||
ByteBuffer buffer = retainable.getByteBuffer();
|
||||
while (BufferUtil.space(buffer) > bufferSize / 2)
|
||||
{
|
||||
int read = BufferUtil.readFrom(input, buffer);
|
||||
if (read < 0)
|
||||
{
|
||||
readEof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
formData.parse(Content.Chunk.from(buffer, false, retainable::release));
|
||||
if (readEof)
|
||||
{
|
||||
formData.parse(Content.Chunk.EOF);
|
||||
break;
|
||||
}
|
||||
Content.Chunk chunk = Content.Chunk.from(ByteBuffer.wrap(buffer, 0, read), false);
|
||||
formData.parse(chunk);
|
||||
chunk.release();
|
||||
}
|
||||
|
||||
return new Parts(formData);
|
||||
Parts parts = new Parts(formData.join());
|
||||
request.setAttribute(Parts.class.getName(), parts);
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,9 +158,9 @@ public class ServletMultiPartFormData
|
|||
{
|
||||
private final List<Part> parts = new ArrayList<>();
|
||||
|
||||
public Parts(MultiPartFormData formData)
|
||||
public Parts(MultiPartFormData.Parts parts)
|
||||
{
|
||||
formData.join().forEach(part -> parts.add(new ServletPart(formData, part)));
|
||||
parts.forEach(part -> this.parts.add(new ServletPart(parts.getMultiPartFormData(), part)));
|
||||
}
|
||||
|
||||
public Part getPart(String name)
|
||||
|
@ -149,22 +181,17 @@ public class ServletMultiPartFormData
|
|||
{
|
||||
private final MultiPartFormData _formData;
|
||||
private final MultiPart.Part _part;
|
||||
private final long _length;
|
||||
private final InputStream _input;
|
||||
|
||||
private ServletPart(MultiPartFormData formData, MultiPart.Part part)
|
||||
{
|
||||
_formData = formData;
|
||||
_part = part;
|
||||
Content.Source content = part.getContent();
|
||||
_length = content.getLength();
|
||||
_input = Content.Source.asInputStream(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException
|
||||
{
|
||||
return _input;
|
||||
return Content.Source.asInputStream(_part.newContentSource());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -188,13 +215,12 @@ public class ServletMultiPartFormData
|
|||
@Override
|
||||
public long getSize()
|
||||
{
|
||||
return _length;
|
||||
return _part.getLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String fileName) throws IOException
|
||||
{
|
||||
// TODO This should simply move a part that is already on the file system.
|
||||
Path filePath = Path.of(fileName);
|
||||
if (!filePath.isAbsolute())
|
||||
filePath = _formData.getFilesDirectory().resolve(filePath).normalize();
|
||||
|
@ -204,8 +230,7 @@ public class ServletMultiPartFormData
|
|||
@Override
|
||||
public void delete() throws IOException
|
||||
{
|
||||
if (_part instanceof MultiPart.PathPart pathPart)
|
||||
pathPart.delete();
|
||||
_part.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -851,7 +851,7 @@ public class ServletRequestState
|
|||
request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass());
|
||||
|
||||
// Set Jetty specific attributes.
|
||||
request.setAttribute(ErrorProcessor.ERROR_EXCEPTION, null);
|
||||
request.setAttribute(ErrorProcessor.ERROR_EXCEPTION, th);
|
||||
|
||||
// Ensure any async lifecycle is ended!
|
||||
_requestState = RequestState.BLOCKING;
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
|
@ -36,6 +37,7 @@ import org.eclipse.jetty.client.ContentResponse;
|
|||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.MultiPartRequestContent;
|
||||
import org.eclipse.jetty.client.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.Response;
|
||||
import org.eclipse.jetty.client.StringRequestContent;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
|
@ -46,7 +48,9 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.http.MultiPart;
|
||||
import org.eclipse.jetty.http.MultiPartFormData;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
|
@ -54,15 +58,20 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class MultiPartServletTest
|
||||
{
|
||||
|
@ -72,18 +81,31 @@ public class MultiPartServletTest
|
|||
private ServerConnector connector;
|
||||
private HttpClient client;
|
||||
private Path tmpDir;
|
||||
private String tmpDirString;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
{
|
||||
tmpDir = Files.createTempDirectory(MultiPartServletTest.class.getSimpleName());
|
||||
tmpDirString = tmpDir.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
private void start(HttpServlet servlet) throws Exception
|
||||
{
|
||||
tmpDir = Files.createTempDirectory(MultiPartServletTest.class.getSimpleName());
|
||||
start(servlet, new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0));
|
||||
}
|
||||
|
||||
server = new Server();
|
||||
private void start(HttpServlet servlet, MultipartConfigElement config) throws Exception
|
||||
{
|
||||
start(servlet, config, null);
|
||||
}
|
||||
|
||||
private void start(HttpServlet servlet, MultipartConfigElement config, ByteBufferPool bufferPool) throws Exception
|
||||
{
|
||||
server = new Server(null, null, bufferPool);
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
MultipartConfigElement config = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
|
||||
MAX_FILE_SIZE, -1, 0);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(server, "/");
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
servletHolder.getRegistration().setMultipartConfig(config);
|
||||
|
@ -109,6 +131,136 @@ public class MultiPartServletTest
|
|||
IO.delete(tmpDir.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargePart() throws Exception
|
||||
{
|
||||
// TODO: Use normal pool when a fix for https://github.com/eclipse/jetty.project/issues/9311 is merged.
|
||||
ByteBufferPool bufferPool = new ByteBufferPool.NonPooling();
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
req.getParameterMap();
|
||||
}
|
||||
}, new MultipartConfigElement(tmpDirString), bufferPool);
|
||||
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 2; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
content.close();
|
||||
|
||||
Response response = listener.get(30, TimeUnit.MINUTES);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
String responseContent = IO.toString(listener.getInputStream());
|
||||
assertThat(responseContent, containsString("Unable to parse form content"));
|
||||
assertThat(responseContent, containsString("Form is larger than max length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManyParts() throws Exception
|
||||
{
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
req.getParameterMap();
|
||||
}
|
||||
}, new MultipartConfigElement(tmpDirString));
|
||||
|
||||
byte[] byteArray = new byte[1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
BytesRequestContent content = new BytesRequestContent(byteArray);
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part" + i, null, null, content));
|
||||
}
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
Response response = listener.get(30, TimeUnit.SECONDS);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
String responseContent = IO.toString(listener.getInputStream());
|
||||
assertThat(responseContent, containsString("Unable to parse form content"));
|
||||
assertThat(responseContent, containsString("Form with too many keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxRequestSize() throws Exception
|
||||
{
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
req.getParameterMap();
|
||||
}
|
||||
}, new MultipartConfigElement(tmpDirString, -1, 1024, 1024 * 1024 * 8));
|
||||
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/requestSizeLimit")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
Throwable writeError = null;
|
||||
try
|
||||
{
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
fail("We should never be able to write all the content.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeError = e;
|
||||
}
|
||||
|
||||
assertThat(writeError, instanceOf(EofException.class));
|
||||
|
||||
// We should get 400 response, for some reason reading the content throws EofException.
|
||||
Response response = listener.get(30, TimeUnit.SECONDS);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleMultiPart() throws Exception
|
||||
{
|
||||
|
@ -237,6 +389,7 @@ public class MultiPartServletTest
|
|||
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
MultiPartFormData formData = new MultiPartFormData(boundary);
|
||||
formData.setMaxParts(1);
|
||||
|
||||
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
|
||||
formData.parse(Content.Chunk.from(ByteBuffer.wrap(IO.readBytes(inputStream)), true));
|
||||
|
@ -245,4 +398,80 @@ public class MultiPartServletTest
|
|||
assertThat(parts.size(), is(1));
|
||||
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleReadFromPart() throws Exception
|
||||
{
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
resp.setContentType("text/plain");
|
||||
for (Part part : req.getParts())
|
||||
{
|
||||
resp.getWriter().println("Part: name=" + part.getName() + ", size=" + part.getSize() + ", content=" + IO.toString(part.getInputStream()));
|
||||
resp.getWriter().println("Part: name=" + part.getName() + ", size=" + part.getSize() + ", content=" + IO.toString(part.getInputStream()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String contentString = "the quick brown fox jumps over the lazy dog, " +
|
||||
"the quick brown fox jumps over the lazy dog";
|
||||
StringRequestContent content = new StringRequestContent(contentString);
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("myPart", null, HttpFields.EMPTY, content));
|
||||
multiPart.close();
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.getContentAsString(), containsString("Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog\n" +
|
||||
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartAsParameter() throws Exception
|
||||
{
|
||||
start(new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
resp.setContentType("text/plain");
|
||||
Map<String, String[]> parameterMap = req.getParameterMap();
|
||||
for (Map.Entry<String, String[]> entry : parameterMap.entrySet())
|
||||
{
|
||||
assertThat(entry.getValue().length, equalTo(1));
|
||||
resp.getWriter().println("Parameter: " + entry.getKey() + "=" + entry.getValue()[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String contentString = "the quick brown fox jumps over the lazy dog, " +
|
||||
"the quick brown fox jumps over the lazy dog";
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part1", null, HttpFields.EMPTY, new StringRequestContent(contentString)));
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part2", null, HttpFields.EMPTY, new StringRequestContent(contentString)));
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part3", null, HttpFields.EMPTY, new StringRequestContent(contentString)));
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("partFileName", "myFile", HttpFields.EMPTY, new StringRequestContent(contentString)));
|
||||
multiPart.close();
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
String responseContent = response.getContentAsString();
|
||||
assertThat(responseContent, containsString("Parameter: part1=" + contentString));
|
||||
assertThat(responseContent, containsString("Parameter: part2=" + contentString));
|
||||
assertThat(responseContent, containsString("Parameter: part3=" + contentString));
|
||||
assertThat(responseContent, not(containsString("Parameter: partFileName=" + contentString)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,8 +92,7 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.hazelcast</groupId>
|
||||
<artifactId>hazelcast-all</artifactId>
|
||||
<version>${hazelcast.version}</version>
|
||||
<artifactId>hazelcast</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
|
||||
/**
|
||||
* ClusteredOrphanedSessionTest
|
||||
*
|
||||
*/
|
||||
public class ClusteredOrphanedSessionTest
|
||||
extends AbstractClusteredOrphanedSessionTest
|
||||
|
|
|
@ -19,10 +19,7 @@ import com.hazelcast.client.HazelcastClient;
|
|||
import com.hazelcast.client.config.ClientConfig;
|
||||
import com.hazelcast.client.config.ClientNetworkConfig;
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.config.JoinConfig;
|
||||
import com.hazelcast.config.MapConfig;
|
||||
import com.hazelcast.config.MulticastConfig;
|
||||
import com.hazelcast.config.NetworkConfig;
|
||||
import com.hazelcast.config.SerializerConfig;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
|
@ -62,7 +59,7 @@ public class HazelcastTestHelper
|
|||
_serializerConfig = new SerializerConfig().setImplementation(new SessionDataSerializer()).setTypeClass(SessionData.class);
|
||||
Config config = new Config();
|
||||
config.setInstanceName(_hazelcastInstanceName);
|
||||
config.setNetworkConfig(new NetworkConfig().setJoin(new JoinConfig().setMulticastConfig(new MulticastConfig().setEnabled(false))));
|
||||
config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false);
|
||||
config.addMapConfig(new MapConfig().setName(_name)).setClassLoader(null);
|
||||
config.getSerializationConfig().addSerializerConfig(_serializerConfig);
|
||||
_instance = Hazelcast.getOrCreateHazelcastInstance(config);
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<jakarta.xml.jaxws.impl.version>4.0.0</jakarta.xml.jaxws.impl.version>
|
||||
<jakarta.websocket.api.version>2.1.0</jakarta.websocket.api.version>
|
||||
|
||||
<jsp.impl.version>10.1.1</jsp.impl.version>
|
||||
<jsp.impl.version>10.1.5</jsp.impl.version>
|
||||
<mail.impl.version>2.0.1</mail.impl.version>
|
||||
|
||||
<sonar.skip>true</sonar.skip>
|
||||
|
|
|
@ -189,18 +189,8 @@ class AsyncContentProducer implements ContentProducer
|
|||
LOG.trace("consumeAll {}", this, x);
|
||||
}
|
||||
failCurrentContent(x);
|
||||
// A specific HttpChannel mechanism must be used as the following code
|
||||
// does not guarantee that the channel will synchronously deliver all
|
||||
// content it already contains:
|
||||
// while (true)
|
||||
// {
|
||||
// HttpInput.Content content = _httpChannel.produceContent();
|
||||
// ...
|
||||
// }
|
||||
// as the HttpChannel's produceContent() contract makes no such promise;
|
||||
// for instance the H2 implementation calls Stream.demand() that may
|
||||
// deliver the content asynchronously. Tests in StreamResetTest cover this.
|
||||
boolean atEof = _httpChannel.failAllContent(x);
|
||||
|
||||
boolean atEof = _httpChannel.getRequest().getCoreRequest().consumeAvailable();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failed all content of http channel EOF={} {}", atEof, this);
|
||||
return atEof;
|
||||
|
|
|
@ -47,6 +47,8 @@ import org.eclipse.jetty.util.thread.AutoLock;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.eclipse.jetty.ee9.nested.ContextHandler.DEFAULT_MAX_FORM_KEYS;
|
||||
|
||||
/**
|
||||
* MultiPartInputStream
|
||||
* <p>
|
||||
|
@ -97,6 +99,8 @@ public class MultiPartFormInputStream
|
|||
private final MultipartConfigElement _config;
|
||||
private final File _contextTmpDir;
|
||||
private final String _contentType;
|
||||
private final int _maxParts;
|
||||
private int _numParts = 0;
|
||||
private volatile Throwable _err;
|
||||
private volatile Path _tmpDir;
|
||||
private volatile boolean _deleteOnExit;
|
||||
|
@ -380,9 +384,20 @@ public class MultiPartFormInputStream
|
|||
* @param in Request input stream
|
||||
* @param contentType Content-Type header
|
||||
* @param config MultipartConfigElement
|
||||
* @param contextTmpDir jakarta.servlet.context.tempdir
|
||||
* @param contextTmpDir javax.servlet.context.tempdir
|
||||
*/
|
||||
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
|
||||
{
|
||||
this(in, contentType, config, contextTmpDir, DEFAULT_MAX_FORM_KEYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in Request input stream
|
||||
* @param contentType Content-Type header
|
||||
* @param config MultipartConfigElement
|
||||
* @param contextTmpDir javax.servlet.context.tempdir
|
||||
*/
|
||||
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
|
||||
{
|
||||
// Must be a multipart request.
|
||||
_contentType = contentType;
|
||||
|
@ -391,6 +406,7 @@ public class MultiPartFormInputStream
|
|||
|
||||
_contextTmpDir = (contextTmpDir != null) ? contextTmpDir : new File(System.getProperty("java.io.tmpdir"));
|
||||
_config = (config != null) ? config : new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
|
||||
_maxParts = maxParts;
|
||||
|
||||
if (in instanceof ServletInputStream)
|
||||
{
|
||||
|
@ -809,6 +825,9 @@ public class MultiPartFormInputStream
|
|||
public void startPart()
|
||||
{
|
||||
reset();
|
||||
_numParts++;
|
||||
if (_maxParts >= 0 && _numParts > _maxParts)
|
||||
throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", _numParts, _maxParts));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1953,7 +1953,21 @@ public class Request implements HttpServletRequest
|
|||
if (config == null)
|
||||
throw new IllegalStateException("No multipart config for servlet");
|
||||
|
||||
_multiParts = newMultiParts(config);
|
||||
int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
|
||||
int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
|
||||
if (_context != null)
|
||||
{
|
||||
ContextHandler contextHandler = _context.getContextHandler();
|
||||
maxFormContentSize = contextHandler.getMaxFormContentSize();
|
||||
maxFormKeys = contextHandler.getMaxFormKeys();
|
||||
}
|
||||
else
|
||||
{
|
||||
maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
|
||||
maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
|
||||
}
|
||||
|
||||
_multiParts = newMultiParts(config, maxFormKeys);
|
||||
Collection<Part> parts = _multiParts.getParts();
|
||||
setNonComplianceViolationsOnRequest();
|
||||
|
||||
|
@ -1987,11 +2001,16 @@ public class Request implements HttpServletRequest
|
|||
else
|
||||
defaultCharset = StandardCharsets.UTF_8;
|
||||
|
||||
long formContentSize = 0;
|
||||
ByteArrayOutputStream os = null;
|
||||
for (Part p : parts)
|
||||
{
|
||||
if (p.getSubmittedFileName() == null)
|
||||
{
|
||||
formContentSize = Math.addExact(formContentSize, p.getSize());
|
||||
if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
|
||||
throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
|
||||
|
||||
// Servlet Spec 3.0 pg 23, parts without filename must be put into params.
|
||||
String charset = null;
|
||||
if (p.getContentType() != null)
|
||||
|
@ -2032,10 +2051,10 @@ public class Request implements HttpServletRequest
|
|||
setAttribute(HttpCompliance.VIOLATIONS_ATTR, violations);
|
||||
}
|
||||
|
||||
private MultiPartFormInputStream newMultiParts(MultipartConfigElement config) throws IOException
|
||||
private MultiPartFormInputStream newMultiParts(MultipartConfigElement config, int maxParts) throws IOException
|
||||
{
|
||||
return new MultiPartFormInputStream(getInputStream(), getContentType(), config,
|
||||
(_context != null ? (File)_context.getAttribute("jakarta.servlet.context.tempdir") : null));
|
||||
(_context != null ? (File)_context.getAttribute("jakarta.servlet.context.tempdir") : null), maxParts);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2347,6 +2347,12 @@ public class RequestTest
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
|
|
|
@ -2367,6 +2367,12 @@ public class ResponseTest
|
|||
return Content.Chunk.EOF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
package org.eclipse.jetty.ee9.proxy;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
|
@ -1359,7 +1358,7 @@ public class ProxyServletTest
|
|||
|
||||
chunk1Latch.countDown();
|
||||
|
||||
assertThrows(EOFException.class, () ->
|
||||
assertThrows(IOException.class, () ->
|
||||
{
|
||||
// Make sure the proxy does not receive chunk2.
|
||||
input.read();
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.eclipse.jetty.client.ContentResponse;
|
|||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.MultiPartRequestContent;
|
||||
import org.eclipse.jetty.client.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.Response;
|
||||
import org.eclipse.jetty.client.StringRequestContent;
|
||||
import org.eclipse.jetty.ee9.nested.HttpChannel;
|
||||
|
@ -42,8 +43,10 @@ import org.eclipse.jetty.http.HttpFields;
|
|||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.MultiPart;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
|
@ -55,10 +58,13 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class MultiPartServletTest
|
||||
{
|
||||
|
@ -69,6 +75,20 @@ public class MultiPartServletTest
|
|||
|
||||
private static final int MAX_FILE_SIZE = 512 * 1024;
|
||||
private static final int LARGE_MESSAGE_SIZE = 1024 * 1024;
|
||||
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 8;
|
||||
|
||||
public static class RequestParameterServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
req.getParameterMap();
|
||||
req.getParts();
|
||||
resp.setStatus(200);
|
||||
resp.getWriter().print("success");
|
||||
resp.getWriter().close();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MultiPartServlet extends HttpServlet
|
||||
{
|
||||
|
@ -119,11 +139,19 @@ public class MultiPartServletTest
|
|||
|
||||
MultipartConfigElement config = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
|
||||
MAX_FILE_SIZE, -1, 1);
|
||||
MultipartConfigElement requestSizedConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
|
||||
-1, MAX_REQUEST_SIZE, 1);
|
||||
MultipartConfigElement defaultConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
|
||||
-1, -1, 1);
|
||||
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
|
||||
contextHandler.setContextPath("/");
|
||||
ServletHolder servletHolder = contextHandler.addServlet(MultiPartServlet.class, "/");
|
||||
servletHolder.getRegistration().setMultipartConfig(config);
|
||||
servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/defaultConfig");
|
||||
servletHolder.getRegistration().setMultipartConfig(defaultConfig);
|
||||
servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/requestSizeLimit");
|
||||
servletHolder.getRegistration().setMultipartConfig(requestSizedConfig);
|
||||
servletHolder = contextHandler.addServlet(MultiPartEchoServlet.class, "/echo");
|
||||
servletHolder.getRegistration().setMultipartConfig(config);
|
||||
|
||||
|
@ -149,6 +177,107 @@ public class MultiPartServletTest
|
|||
IO.delete(tmpDir.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargePart() throws Exception
|
||||
{
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 2; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
content.close();
|
||||
|
||||
Response response = listener.get(2, TimeUnit.MINUTES);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
String responseContent = IO.toString(listener.getInputStream());
|
||||
assertThat(responseContent, containsString("Unable to parse form content"));
|
||||
assertThat(responseContent, containsString("Form is larger than max length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManyParts() throws Exception
|
||||
{
|
||||
byte[] byteArray = new byte[1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
BytesRequestContent content = new BytesRequestContent(byteArray);
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part" + i, null, null, content));
|
||||
}
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
Response response = listener.get(30, TimeUnit.SECONDS);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
String responseContent = IO.toString(listener.getInputStream());
|
||||
assertThat(responseContent, containsString("Unable to parse form content"));
|
||||
assertThat(responseContent, containsString("Form with too many keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxRequestSize() throws Exception
|
||||
{
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/requestSizeLimit")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
Throwable writeError = null;
|
||||
try
|
||||
{
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
fail("We should never be able to write all the content.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeError = e;
|
||||
}
|
||||
|
||||
assertThat(writeError, instanceOf(EofException.class));
|
||||
|
||||
// We should get 400 response, for some reason reading the content throws EofException.
|
||||
Response response = listener.get(30, TimeUnit.SECONDS);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTempFilesDeletedOnError() throws Exception
|
||||
{
|
||||
|
|
|
@ -91,8 +91,7 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.hazelcast</groupId>
|
||||
<artifactId>hazelcast-all</artifactId>
|
||||
<version>${hazelcast.version}</version>
|
||||
<artifactId>hazelcast</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
|
|
|
@ -19,10 +19,7 @@ import com.hazelcast.client.HazelcastClient;
|
|||
import com.hazelcast.client.config.ClientConfig;
|
||||
import com.hazelcast.client.config.ClientNetworkConfig;
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.config.JoinConfig;
|
||||
import com.hazelcast.config.MapConfig;
|
||||
import com.hazelcast.config.MulticastConfig;
|
||||
import com.hazelcast.config.NetworkConfig;
|
||||
import com.hazelcast.config.SerializerConfig;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
|
@ -59,7 +56,7 @@ public class HazelcastTestHelper
|
|||
_serializerConfig = new SerializerConfig().setImplementation(new SessionDataSerializer()).setTypeClass(SessionData.class);
|
||||
Config config = new Config();
|
||||
config.setInstanceName(_hazelcastInstanceName);
|
||||
config.setNetworkConfig(new NetworkConfig().setJoin(new JoinConfig().setMulticastConfig(new MulticastConfig().setEnabled(false))));
|
||||
config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false);
|
||||
config.addMapConfig(new MapConfig().setName(_name)).setClassLoader(null);
|
||||
config.getSerializationConfig().addSerializerConfig(_serializerConfig);
|
||||
_instance = Hazelcast.getOrCreateHazelcastInstance(config);
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<dependency>
|
||||
<groupId>com.hazelcast</groupId>
|
||||
<artifactId>hazelcast</artifactId>
|
||||
<version>${hazelcast.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -49,7 +49,7 @@
|
|||
<guava.version>31.1-jre</guava.version>
|
||||
<guice.version>5.1.0</guice.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
<hazelcast.version>4.2.6</hazelcast.version>
|
||||
<hazelcast.version>5.2.1</hazelcast.version>
|
||||
<infinispan.protostream.version>4.6.0.Final</infinispan.protostream.version>
|
||||
<infinispan.version>11.0.17.Final</infinispan.version>
|
||||
<jackson.version>2.14.2</jackson.version>
|
||||
|
@ -977,6 +977,11 @@
|
|||
<artifactId>json-simple</artifactId>
|
||||
<version>${json-simple.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.hazelcast</groupId>
|
||||
<artifactId>hazelcast</artifactId>
|
||||
<version>${hazelcast.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.openpojo</groupId>
|
||||
<artifactId>openpojo</artifactId>
|
||||
|
|
|
@ -194,7 +194,6 @@ public class DistributionTests extends AbstractJettyHomeTest
|
|||
}
|
||||
}
|
||||
|
||||
@Disabled //TODO glassfish-jstl not working in jpms
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"ee10"})
|
||||
public void testSimpleWebAppWithJSPOnModulePath(String env) throws Exception
|
||||
|
@ -237,7 +236,6 @@ public class DistributionTests extends AbstractJettyHomeTest
|
|||
|
||||
response = client.GET("http://localhost:" + port + "/test/jstl.jsp");
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
System.err.println(response.getContentAsString());
|
||||
assertThat(response.getContentAsString(), containsString("JSTL Example"));
|
||||
assertThat(response.getContentAsString(), not(containsString("<c:")));
|
||||
}
|
||||
|
@ -1105,6 +1103,59 @@ public class DistributionTests extends AbstractJettyHomeTest
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestLogFormatWithSpaces() throws Exception
|
||||
{
|
||||
Path jettyBase = newTestJettyBaseDirectory();
|
||||
String jettyVersion = System.getProperty("jettyVersion");
|
||||
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
|
||||
.jettyVersion(jettyVersion)
|
||||
.jettyBase(jettyBase)
|
||||
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
|
||||
.build();
|
||||
|
||||
String[] args1 = {"--add-module=server,http,deploy,requestlog"};
|
||||
try (JettyHomeTester.Run run1 = distribution.start(args1))
|
||||
{
|
||||
assertTrue(run1.awaitFor(10, TimeUnit.SECONDS));
|
||||
assertEquals(0, run1.getExitValue());
|
||||
|
||||
// Setup custom format string with spaces
|
||||
Path requestLogIni = distribution.getJettyBase().resolve("start.d/requestlog.ini");
|
||||
List<String> lines = List.of(
|
||||
"--module=requestlog",
|
||||
"jetty.requestlog.filePath=logs/test.request.log",
|
||||
"jetty.requestlog.formatString=%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t [foo space here] \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\""
|
||||
);
|
||||
Files.write(requestLogIni, lines, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
|
||||
int port = distribution.freePort();
|
||||
String[] args2 = {
|
||||
"jetty.http.port=" + port,
|
||||
};
|
||||
try (JettyHomeTester.Run run2 = distribution.start(args2))
|
||||
{
|
||||
assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", 10, TimeUnit.SECONDS));
|
||||
startHttpClient(false);
|
||||
|
||||
String uri = "http://localhost:" + port + "/test";
|
||||
|
||||
// Generate a request
|
||||
ContentResponse response = client.GET(uri + "/");
|
||||
// Don't really care about the result, as any request should be logged in the requestlog
|
||||
// We are just asserting a status here to ensure that the request is complete
|
||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||
|
||||
Path requestLog = distribution.getJettyBase().resolve("logs/test.request.log");
|
||||
List<String> loggedLines = Files.readAllLines(requestLog, StandardCharsets.UTF_8);
|
||||
for (String loggedLine: loggedLines)
|
||||
{
|
||||
assertThat(loggedLine, containsString(" [foo space here] "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFastCGIProxying() throws Exception
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue