#10330 - Fix broken EE9 DefaultServlet range requests

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2023-08-17 14:57:57 +02:00
parent f4b97fbfb8
commit 7f0a356585
4 changed files with 215 additions and 51 deletions

View File

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Enumeration; import java.util.Enumeration;
@ -232,9 +233,16 @@ public class ResourceService
pathInfo = request.getPathInfo(); pathInfo = request.getPathInfo();
// Is this a Range request? // Is this a Range request?
reqRanges = request.getHeaders(HttpHeader.RANGE.asString()); if (_acceptRanges)
if (!hasDefinedRange(reqRanges)) {
reqRanges = null; reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
if (!hasDefinedRange(reqRanges))
reqRanges = null;
}
else if (request.getHeader(HttpHeader.RANGE.asString()) != null)
{
response.setHeader(HttpHeader.ACCEPT_RANGES.asString(), "none");
}
} }
String pathInContext = URIUtil.addPaths(servletPath, pathInfo); String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
@ -864,28 +872,37 @@ public class ResourceService
private static void writeContent(HttpContent content, OutputStream out, long start, long contentLength) throws IOException private static void writeContent(HttpContent content, OutputStream out, long start, long contentLength) throws IOException
{ {
// Is the write for the whole content? // attempt efficient ByteBuffer based write
if (start == 0 && content.getResource().length() == contentLength) ByteBuffer buffer = content.getByteBuffer();
if (buffer != null)
{ {
// attempt efficient ByteBuffer based write for whole content // no need to modify buffer pointers when whole content is requested
ByteBuffer buffer = content.getByteBuffer(); if (start != 0 || content.getResource().length() != contentLength)
if (buffer != null)
{ {
BufferUtil.writeTo(buffer, out); buffer = buffer.asReadOnlyBuffer();
return; buffer.position((int)(buffer.position() + start));
} buffer.limit((int)(buffer.position() + contentLength));
try (InputStream input = content.getResource().newInputStream())
{
IO.copy(input, out);
return;
} }
BufferUtil.writeTo(buffer, out);
return;
} }
// Use a ranged writer // Use a ranged writer if resource backed by path
try (SeekableByteChannelRangeWriter rangeWriter = new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getResource().getPath()))) Path path = content.getResource().getPath();
if (path != null)
{ {
rangeWriter.writeTo(out, start, contentLength); try (SeekableByteChannelRangeWriter rangeWriter = new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(path)))
{
rangeWriter.writeTo(out, start, contentLength);
}
return;
}
// Perform ranged write
try (InputStream input = content.getResource().newInputStream())
{
input.skipNBytes(start);
IO.copy(input, out, contentLength);
} }
} }

View File

@ -13,29 +13,25 @@
package org.eclipse.jetty.ee9.nested.resource; package org.eclipse.jetty.ee9.nested.resource;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import org.eclipse.jetty.http.content.HttpContent; import org.eclipse.jetty.http.content.HttpContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Range Writer selection for HttpContent * Range Writer selection for HttpContent
*/ */
public class HttpContentRangeWriter public class HttpContentRangeWriter
{ {
private static final Logger LOG = LoggerFactory.getLogger(HttpContentRangeWriter.class);
/** /**
* Obtain a new RangeWriter for the supplied HttpContent. * Obtain a new RangeWriter for the supplied HttpContent.
* *
* @param content the HttpContent to base RangeWriter on * @param content the HttpContent to base RangeWriter on
* @return the RangeWriter best suited for the supplied HttpContent * @return the RangeWriter best suited for the supplied HttpContent
*/ */
public static RangeWriter newRangeWriter(HttpContent content) throws IOException public static RangeWriter newRangeWriter(HttpContent content)
{ {
Objects.requireNonNull(content, "HttpContent"); Objects.requireNonNull(content, "HttpContent");
@ -44,6 +40,12 @@ public class HttpContentRangeWriter
if (buffer != null) if (buffer != null)
return new ByteBufferRangeWriter(buffer); return new ByteBufferRangeWriter(buffer);
return new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(content.getResource().getPath())); // Try path's SeekableByteChannel
Path path = content.getResource().getPath();
if (path != null)
return new SeekableByteChannelRangeWriter(() -> Files.newByteChannel(path));
// Fallback to InputStream
return new InputStreamRangeWriter(() -> content.getResource().newInputStream());
} }
} }

View File

@ -245,35 +245,39 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
if (cc != null) if (cc != null)
_resourceService.setCacheControl(new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cc)); _resourceService.setCacheControl(new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cc));
// Try to get factory from ServletContext attribute. // Create HttpContentFactory if none already set
HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName()); if (_resourceService.getHttpContentFactory() == null)
if (contentFactory == null)
{ {
contentFactory = new ResourceHttpContentFactory(this, _mimeTypes); // Try to get factory from ServletContext attribute.
if (_useFileMappedBuffer) HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
contentFactory = new FileMappingHttpContentFactory(contentFactory); if (contentFactory == null)
contentFactory = new VirtualHttpContentFactory(contentFactory, _styleSheet, "text/css");
contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
int maxCacheSize = getInitInt("maxCacheSize", -2);
int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
int maxCachedFiles = getInitInt("maxCachedFiles", -2);
long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{ {
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler); contentFactory = new ResourceHttpContentFactory(this, _mimeTypes);
_cachingContentFactory = new ValidatingCachingHttpContentFactory(contentFactory, if (_useFileMappedBuffer)
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool); contentFactory = new FileMappingHttpContentFactory(contentFactory);
contentFactory = _cachingContentFactory; contentFactory = new VirtualHttpContentFactory(contentFactory, _styleSheet, "text/css");
if (maxCacheSize >= 0) contentFactory = new PreCompressedHttpContentFactory(contentFactory, _resourceService.getPrecompressedFormats());
_cachingContentFactory.setMaxCacheSize(maxCacheSize);
if (maxCachedFileSize >= -1) int maxCacheSize = getInitInt("maxCacheSize", -2);
_cachingContentFactory.setMaxCachedFileSize(maxCachedFileSize); int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
if (maxCachedFiles >= -1) int maxCachedFiles = getInitInt("maxCachedFiles", -2);
_cachingContentFactory.setMaxCachedFiles(maxCachedFiles); long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
{
ByteBufferPool bufferPool = getByteBufferPool(_contextHandler);
_cachingContentFactory = new ValidatingCachingHttpContentFactory(contentFactory,
(cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
contentFactory = _cachingContentFactory;
if (maxCacheSize >= 0)
_cachingContentFactory.setMaxCacheSize(maxCacheSize);
if (maxCachedFileSize >= -1)
_cachingContentFactory.setMaxCachedFileSize(maxCachedFileSize);
if (maxCachedFiles >= -1)
_cachingContentFactory.setMaxCachedFiles(maxCachedFiles);
}
} }
_resourceService.setHttpContentFactory(contentFactory);
} }
_resourceService.setHttpContentFactory(contentFactory);
_resourceService.setWelcomeFactory(this); _resourceService.setWelcomeFactory(this);
List<String> gzipEquivalentFileExtensions = new ArrayList<>(); List<String> gzipEquivalentFileExtensions = new ArrayList<>();

View File

@ -18,6 +18,7 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
@ -45,6 +46,7 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.content.ResourceHttpContent;
import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.AllowedResourceAliasChecker; import org.eclipse.jetty.server.AllowedResourceAliasChecker;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
@ -55,9 +57,12 @@ import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.FileSystemPool; import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.ResourceFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -2267,6 +2272,142 @@ public class DefaultServletTest
assertThat(response.get(HttpHeader.ALLOW), is("GET, HEAD, OPTIONS")); assertThat(response.get(HttpHeader.ALLOW), is("GET, HEAD, OPTIONS"));
} }
@Test
public void testMemoryResourceRange() throws Exception
{
Resource memResource = ResourceFactory.root().newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Range: bytes=10-12\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PARTIAL_CONTENT_206));
assertThat(response.get(HttpHeader.CONTENT_LENGTH), is("3"));
assertThat(response.getContent(), is("too"));
}
@Test
public void testMemoryResourceMultipleRanges() throws Exception
{
Resource memResource = ResourceFactory.root().newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Range: bytes=5-8, 10-12\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PARTIAL_CONTENT_206));
assertThat(response.get(HttpHeader.CONTENT_LENGTH), notNullValue());
assertThat(response.getContent(), Matchers.stringContainsInOrder(
"Content-Type: text/plain", "Content-Range: bytes 5-8/17", "2 to",
"Content-Type: text/plain", "Content-Range: bytes 10-12/17", "too")
);
}
@Test
public void testMemoryResourceRangeUsingBufferedHttpContent() throws Exception
{
Resource memResource = ResourceFactory.root().newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = BufferUtil.toBuffer(getResource(), false);
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Range: bytes=10-12\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PARTIAL_CONTENT_206));
assertThat(response.get(HttpHeader.CONTENT_LENGTH), is("3"));
assertThat(response.getContent(), is("too"));
}
@Test
public void testMemoryResourceMultipleRangesUsingBufferedHttpContent() throws Exception
{
Resource memResource = ResourceFactory.root().newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain")
{
final ByteBuffer buffer = BufferUtil.toBuffer(getResource(), false);
@Override
public ByteBuffer getByteBuffer()
{
return buffer;
}
});
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Range: bytes=5-8, 10-12\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.PARTIAL_CONTENT_206));
assertThat(response.get(HttpHeader.CONTENT_LENGTH), notNullValue());
assertThat(response.getContent(), Matchers.stringContainsInOrder(
"Content-Type: text/plain", "Content-Range: bytes 5-8/17", "2 to",
"Content-Type: text/plain", "Content-Range: bytes 10-12/17", "too")
);
}
@Test
public void testNotAcceptRanges() throws Exception
{
Resource memResource = ResourceFactory.root().newMemoryResource(getClass().getResource("/contextResources/test.txt"));
ResourceService resourceService = new ResourceService();
resourceService.setHttpContentFactory(path -> new ResourceHttpContent(memResource, "text/plain"));
resourceService.setAcceptRanges(false);
DefaultServlet defaultServlet = new DefaultServlet(resourceService);
context.addServlet(new ServletHolder(defaultServlet), "/");
String rawResponse = connector.getResponse("""
GET /context/ HTTP/1.1\r
Host: local\r
Range: bytes=10-12\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.get(HttpHeader.CONTENT_LENGTH), is("17"));
assertThat(response.get(HttpHeader.ACCEPT_RANGES), is("none"));
assertThat(response.getContent(), is("Test 2 to too two"));
}
public static class OutputFilter implements Filter public static class OutputFilter implements Filter
{ {
@Override @Override