jetty-9 improved range request processing and tests

This commit is contained in:
Greg Wilkins 2012-08-27 12:17:40 +10:00
parent 6d2fb580a1
commit d8b287134f
7 changed files with 300 additions and 114 deletions

View File

@ -183,6 +183,7 @@ public class HttpOutput extends ServletOutputStream
if (_aggregate == null)
_aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), false);
System.err.println(BufferUtil.toDetailString(_aggregate));
BufferUtil.append(_aggregate, (byte)b);
_written++;

View File

@ -44,6 +44,9 @@ import org.eclipse.jetty.util.log.Logger;
* </PRE>
*
* Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2
* <p>
* And yes the spec does strangely say that while 10-20, is bytes 10 to 20 and 10- is bytes 10 until the end that -20 IS NOT bytes 0-20, but the last 20 bytes of the content.
*
* @version $version$
*
*/

View File

@ -941,6 +941,9 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory
in.skip(start-pos);
pos=start;
}
System.err.println("PART "+ibr);
IO.copy(in,multi,size);
pos+=size;
}

View File

@ -0,0 +1,273 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import junit.framework.AssertionFailedError;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.OS;
import org.eclipse.jetty.toolchain.test.TestingDir;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class DefaultServletRangesTest
{
public static final String DATA = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWZYZ!@#$%^&*()_+/.,[]";
@Rule
public TestingDir testdir = new TestingDir();
private Server server;
private LocalConnector connector;
private ServletContextHandler context;
@Before
public void init() throws Exception
{
server = new Server();
server.setSendServerVersion(false);
connector = new LocalConnector(server);
context = new ServletContextHandler();
context.setContextPath("/context");
context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"});
server.setHandler(context);
server.addConnector(connector);
testdir.ensureEmpty();
File resBase = testdir.getFile("docroot");
FS.ensureDirExists(resBase);
File data = new File(resBase, "data.txt");
createFile(data, DATA);
String resBasePath = resBase.getAbsolutePath();
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/");
defholder.setInitParameter("acceptRanges", "true");
defholder.setInitParameter("resourceBase", resBasePath);
server.start();
}
@After
public void destroy() throws Exception
{
server.stop();
server.join();
}
@Test
public void testNoRangeRequests() throws Exception
{
String response;
response= connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"\r\n");
assertResponseContains("200 OK", response);
assertResponseContains("Accept-Ranges: bytes", response);
assertResponseContains(DATA,response);
}
@Test
public void testPrefixRangeRequests() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=0-9\r\n" +
"\r\n");
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: text/plain", response);
assertResponseContains("Content-Length: 10", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
assertResponseContains(DATA.substring(0,10), response);
}
@Test
public void testSingleRangeRequests() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=3-9\r\n" +
"\r\n");
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: text/plain", response);
assertResponseContains("Content-Length: 7", response);
assertResponseContains("Content-Range: bytes 3-9/80", response);
assertResponseContains(DATA.substring(3,10), response);
}
@Test
public void testMultipleRangeRequests() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=0-9,20-29,40-49\r\n" +
"\r\n");
int start = response.indexOf("--jetty");
String body = response.substring(start);
String boundary = body.substring(0, body.indexOf("\r\n"));
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
assertResponseContains("Content-Range: bytes 20-29/80", response);
assertResponseContains("Content-Range: bytes 40-49/80", response);
assertResponseContains("Content-Length: " + body.length(), response);
assertResponseContains(DATA.substring(0,10), response);
assertResponseContains(DATA.substring(20,30), response);
assertResponseContains(DATA.substring(40,50), response);
assertTrue(body.endsWith(boundary + "--\r\n"));
}
@Test
public void testOpenEndRange() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=20-\r\n" +
"\r\n");
assertResponseContains("206 Partial", response);
assertResponseNotContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 20-79/80", response);
assertResponseContains("Content-Length: 60", response);
assertResponseContains(DATA.substring(60), response);
}
@Test
public void testOpenStartRange() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=-20\r\n" +
"\r\n");
assertResponseContains("206 Partial", response);
assertResponseNotContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 60-79/80", response); // yes the spec says it is these bytes
assertResponseContains("Content-Length: 20", response);
assertResponseContains(DATA.substring(60), response);
}
@Test
public void testUnsatisfiableRanges() throws Exception
{
String response;
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n"+
"Range: bytes=100-110\r\n" +
"\r\n");
assertResponseContains("416 Requested Range Not Satisfiable", response);
}
private void createFile(File file, String str) throws IOException
{
FileOutputStream out = null;
try
{
out = new FileOutputStream(file);
out.write(str.getBytes(StringUtil.__UTF8));
out.flush();
}
finally
{
IO.close(out);
}
}
private void assertResponseNotContains(String forbidden, String response)
{
Assert.assertThat(response,Matchers.not(Matchers.containsString(forbidden)));
}
private int assertResponseContains(String expected, String response)
{
Assert.assertThat(response,Matchers.containsString(expected));
return response.indexOf(expected);
}
private void deleteFile(File file) throws IOException
{
if (OS.IS_WINDOWS)
{
// Windows doesn't seem to like to delete content that was recently created
// Attempt a delete and if it fails, attempt a rename
boolean deleted = file.delete();
if (!deleted)
{
File deletedDir = MavenTestingUtils.getTargetFile(".deleted");
FS.ensureDirExists(deletedDir);
File dest = File.createTempFile(file.getName(), "deleted", deletedDir);
boolean renamed = file.renameTo(dest);
if (!renamed)
System.err.println("WARNING: unable to move file out of the way: " + file.getName());
}
}
else
{
Assert.assertTrue("Deleting: " + file.getName(), file.delete());
}
}
}

View File

@ -39,6 +39,7 @@ import org.eclipse.jetty.toolchain.test.OS;
import org.eclipse.jetty.toolchain.test.TestingDir;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -102,9 +103,7 @@ public class DefaultServletTest
defholder.setInitParameter("resourceBase", resBasePath);
StringBuffer req1 = new StringBuffer();
req1.append("GET /context/;JSESSIONID=1234567890 HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("\n");
req1.append("GET /context/;JSESSIONID=1234567890 HTTP/1.0\n\n");
String response = connector.getResponses(req1.toString());
@ -144,8 +143,7 @@ public class DefaultServletTest
* Intentionally bad request URI. Sending a non-encoded URI with typically encoded characters '<', '>', and
* '"'.
*/
req1.append("GET /context/;<script>window.alert(\"hi\");</script> HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("GET /context/;<script>window.alert(\"hi\");</script> HTTP/1.0\n");
req1.append("\n");
String response = connector.getResponses(req1.toString());
@ -460,86 +458,6 @@ public class DefaultServletTest
}
}
@Test
public void testRangeRequests() throws Exception
{
testdir.ensureEmpty();
File resBase = testdir.getFile("docroot");
FS.ensureDirExists(resBase);
File data = new File(resBase, "data.txt");
createFile(data, "01234567890123456789012345678901234567890123456789012345678901234567890123456789");
String resBasePath = resBase.getAbsolutePath();
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/");
defholder.setInitParameter("acceptRanges", "true");
defholder.setInitParameter("resourceBase", resBasePath);
String response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n");
assertResponseContains("200 OK", response);
assertResponseContains("Accept-Ranges: bytes", response);
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Range: bytes=0-9\r\n" +
"\r\n");
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: text/plain", response);
assertResponseContains("Content-Length: 10", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Range: bytes=0-9,20-29,40-49\r\n" +
"\r\n");
int start = response.indexOf("--jetty");
String body = response.substring(start);
String boundary = body.substring(0, body.indexOf("\r\n"));
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
assertResponseContains("Content-Range: bytes 20-29/80", response);
assertResponseContains("Content-Length: " + body.length(), response);
assertTrue(body.endsWith(boundary + "--\r\n"));
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Range: bytes=0-9,20-29,40-49,70-79\r\n" +
"\r\n");
start = response.indexOf("--jetty");
body = response.substring(start);
boundary = body.substring(0, body.indexOf("\r\n"));
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
assertResponseContains("Content-Range: bytes 20-29/80", response);
assertResponseContains("Content-Range: bytes 70-79/80", response);
assertResponseContains("Content-Length: " + body.length(), response);
assertTrue(body.endsWith(boundary + "--\r\n"));
response = connector.getResponses(
"GET /context/data.txt HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Range: bytes=0-9,20-29,40-49,60-60,70-79\r\n" +
"\r\n");
start = response.indexOf("--jetty");
body = response.substring(start);
boundary = body.substring(0, body.indexOf("\r\n"));
assertResponseContains("206 Partial", response);
assertResponseContains("Content-Type: multipart/byteranges; boundary=", response);
assertResponseContains("Content-Range: bytes 0-9/80", response);
assertResponseContains("Content-Range: bytes 20-29/80", response);
assertResponseContains("Content-Range: bytes 60-60/80", response);
assertResponseContains("Content-Range: bytes 70-79/80", response);
assertResponseContains("Content-Length: " + body.length(), response);
assertTrue(body.endsWith(boundary + "--\r\n"));
}
@Test
public void testFiltered() throws Exception
{
@ -558,12 +476,12 @@ public class DefaultServletTest
defholder.setInitParameter("gzip", "false");
defholder.setInitParameter("resourceBase", resBasePath);
String response = connector.getResponses("GET /context/data0.txt HTTP/1.1\r\nHost:localhost:8080\r\n\r\n");
String response = connector.getResponses("GET /context/data0.txt HTTP/1.0\r\n\r\n");
assertResponseContains("Content-Length: 12", response);
assertResponseNotContains("Extra Info", response);
context.addFilter(OutputFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST));
response = connector.getResponses("GET /context/data0.txt HTTP/1.1\r\nHost:localhost:8080\r\n\r\n");
response = connector.getResponses("GET /context/data0.txt HTTP/1.0\r\n\r\n");
assertResponseContains("Content-Length: 2", response); // 20 something long
assertResponseContains("Extra Info", response);
assertResponseNotContains("Content-Length: 12", response);
@ -572,7 +490,7 @@ public class DefaultServletTest
context.getServletHandler().setFilters(new FilterHolder[]{});
context.addFilter(WriterFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST));
response = connector.getResponses("GET /context/data0.txt HTTP/1.1\r\nHost:localhost:8080\r\n\r\n");
response = connector.getResponses("GET /context/data0.txt HTTP/1.0\r\n\r\n");
assertResponseContains("Content-Length: 2", response); // 20 something long
assertResponseContains("Extra Info", response);
assertResponseNotContains("Content-Length: 12", response);
@ -629,33 +547,13 @@ public class DefaultServletTest
private void assertResponseNotContains(String forbidden, String response)
{
int idx = response.indexOf(forbidden);
if (idx != (-1))
{
// Found (when should not have)
StringBuffer err = new StringBuffer();
err.append("Response contain forbidden string \"").append(forbidden).append("\"");
err.append("\n").append(response);
System.err.println(err);
throw new AssertionFailedError(err.toString());
}
Assert.assertThat(response,Matchers.not(Matchers.containsString(forbidden)));
}
private int assertResponseContains(String expected, String response)
{
int idx = response.indexOf(expected);
if (idx == (-1))
{
// Not found
StringBuffer err = new StringBuffer();
err.append("Response does not contain expected string \"").append(expected).append("\"");
err.append("\n").append(response);
System.err.println(err);
throw new AssertionFailedError(err.toString());
}
return idx;
Assert.assertThat(response,Matchers.containsString(expected));
return response.indexOf(expected);
}
private void deleteFile(File file) throws IOException

View File

@ -344,8 +344,8 @@ public class BufferUtil
public static void append(ByteBuffer to, byte b)
{
int limit=to.limit();
to.put(limit,b);
to.limit(limit+1);
to.put(limit,b);
}
/* ------------------------------------------------------------ */
@ -359,6 +359,7 @@ public class BufferUtil
needed=needed-channel.read(buffer);
}
/* ------------------------------------------------------------ */
public static void readFrom(InputStream is, int needed, ByteBuffer buffer) throws IOException
{
ByteBuffer tmp = allocate(8192);

View File

@ -58,8 +58,6 @@ public class MultiPartOutputStream extends FilterOutputStream
inPart=false;
}
/* ------------------------------------------------------------ */
/** End the current part.
* @exception IOException IOException
@ -124,6 +122,15 @@ public class MultiPartOutputStream extends FilterOutputStream
}
out.write(__CRLF);
}
/* ------------------------------------------------------------ */
@Override
public void write(byte[] b, int off, int len) throws IOException
{
out.write(b,off,len);
}
}