Bug 371798 - potential pipelining issue
+ Adding testcase for gzip + pipelining issue reported in bugzilla. Created scenario where 2 requests are made, with 2nd request overlapping the first response. The first response is also gzip'd
This commit is contained in:
parent
566eec65ce
commit
766ff7cf19
|
@ -0,0 +1,183 @@
|
||||||
|
package org.eclipse.jetty.servlets;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.DigestOutputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.Connector;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||||
|
import org.eclipse.jetty.servlet.FilterHolder;
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
import org.eclipse.jetty.servlets.gzip.Hex;
|
||||||
|
import org.eclipse.jetty.servlets.gzip.NoOpOutputStream;
|
||||||
|
import org.eclipse.jetty.toolchain.test.IO;
|
||||||
|
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the effects of Gzip filtering when in the context of HTTP/1.1 Pipelining.
|
||||||
|
*/
|
||||||
|
public class GzipWithPipeliningTest
|
||||||
|
{
|
||||||
|
private Server server;
|
||||||
|
private URI serverUri;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void startServer() throws Exception
|
||||||
|
{
|
||||||
|
// Configure Server
|
||||||
|
server = new Server(0);
|
||||||
|
|
||||||
|
ServletContextHandler context = new ServletContextHandler();
|
||||||
|
context.setContextPath("/");
|
||||||
|
|
||||||
|
DefaultServlet servlet = new DefaultServlet();
|
||||||
|
ServletHolder holder = new ServletHolder(servlet);
|
||||||
|
holder.setInitParameter("resourceBase",MavenTestingUtils.getTestResourcesDir().getAbsolutePath());
|
||||||
|
context.addServlet(holder,"/");
|
||||||
|
|
||||||
|
FilterHolder filter = context.addFilter(GzipFilter.class,"/*",0);
|
||||||
|
filter.setInitParameter("mimeTypes","text/plain");
|
||||||
|
|
||||||
|
server.setHandler(context);
|
||||||
|
|
||||||
|
// Start Server
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
Connector conn = server.getConnectors()[0];
|
||||||
|
String host = conn.getHost();
|
||||||
|
if (host == null)
|
||||||
|
{
|
||||||
|
host = "localhost";
|
||||||
|
}
|
||||||
|
int port = conn.getLocalPort();
|
||||||
|
serverUri = new URI(String.format("ws://%s:%d/",host,port));
|
||||||
|
// System.out.printf("Server URI: %s%n",serverUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void stopServer() throws Exception
|
||||||
|
{
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore
|
||||||
|
public void testGzipThenImagePipelining() throws Exception
|
||||||
|
{
|
||||||
|
PipelineHelper client = new PipelineHelper(serverUri);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File txtFile = MavenTestingUtils.getTestResourceFile("lots-of-fantasy-names.txt");
|
||||||
|
File pngFile = MavenTestingUtils.getTestResourceFile("jetty_logo.png");
|
||||||
|
|
||||||
|
// Size of content, as it exists on disk, without gzip compression.
|
||||||
|
long rawsize = txtFile.length() + pngFile.length();
|
||||||
|
Assert.assertThat("Ensure that we have sufficient file size to trigger chunking",rawsize,greaterThan(300000L));
|
||||||
|
|
||||||
|
String respHeader;
|
||||||
|
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
// Request text that will be gzipped + chunked in the response
|
||||||
|
client.issueGET("/lots-of-fantasy-names.txt",true);
|
||||||
|
|
||||||
|
respHeader = client.readResponseHeader();
|
||||||
|
System.out.println("Response Header #1 --\n" + respHeader);
|
||||||
|
Assert.assertThat("Content-Encoding should be gzipped",respHeader,containsString("Content-Encoding: gzip\r\n"));
|
||||||
|
Assert.assertThat("Transfer-Encoding should be chunked",respHeader,containsString("Transfer-Encoding: chunked\r\n"));
|
||||||
|
|
||||||
|
// Sha1tracking for First Request
|
||||||
|
MessageDigest digestMain = MessageDigest.getInstance("SHA1");
|
||||||
|
DigestOutputStream digesterMain = new DigestOutputStream(new NoOpOutputStream(),digestMain);
|
||||||
|
GZIPOutputStream gziperMain = new GZIPOutputStream(digesterMain);
|
||||||
|
|
||||||
|
long chunkSize = client.readChunkSize();
|
||||||
|
System.out.println("Chunk Size: " + chunkSize);
|
||||||
|
|
||||||
|
// Read only 20% - intentionally a partial read.
|
||||||
|
System.out.println("Attempting to read partial content ...");
|
||||||
|
int readBytes = client.readBody(gziperMain,(int)((float)chunkSize * 0.20f));
|
||||||
|
System.out.printf("Read %,d bytes%n",readBytes);
|
||||||
|
|
||||||
|
// Issue another request
|
||||||
|
client.issueGET("/jetty_logo.png",true);
|
||||||
|
|
||||||
|
// Finish reading chunks
|
||||||
|
System.out.println("Finish reading reamaining chunks ...");
|
||||||
|
String line;
|
||||||
|
chunkSize = chunkSize - readBytes;
|
||||||
|
while (chunkSize > 0)
|
||||||
|
{
|
||||||
|
readBytes = client.readBody(gziperMain,(int)chunkSize);
|
||||||
|
System.out.printf("Read %,d bytes%n",readBytes);
|
||||||
|
line = client.readLine();
|
||||||
|
Assert.assertThat("Chunk delim should be an empty line with CR+LF",line,is(""));
|
||||||
|
chunkSize = client.readChunkSize();
|
||||||
|
System.out.printf("Next Chunk: (0x%X) %,d bytes%n",chunkSize,chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inter-pipeline delim
|
||||||
|
line = client.readLine();
|
||||||
|
Assert.assertThat("Inter-pipeline delim should be an empty line with CR+LF",line,is(""));
|
||||||
|
|
||||||
|
// Read 2nd request http response header
|
||||||
|
respHeader = client.readResponseHeader();
|
||||||
|
System.out.println("Response Header #2 --\n" + respHeader);
|
||||||
|
Assert.assertThat("Content-Encoding should NOT be gzipped",respHeader,not(containsString("Content-Encoding: gzip\r\n")));
|
||||||
|
Assert.assertThat("Transfer-Encoding should NOT be chunked",respHeader,not(containsString("Transfer-Encoding: chunked\r\n")));
|
||||||
|
|
||||||
|
// Sha1tracking for 2nd Request
|
||||||
|
MessageDigest digestImg = MessageDigest.getInstance("SHA1");
|
||||||
|
DigestOutputStream digesterImg = new DigestOutputStream(new NoOpOutputStream(),digestImg);
|
||||||
|
|
||||||
|
// Read 2nd request body
|
||||||
|
int contentLength = client.getContentLength(respHeader);
|
||||||
|
Assert.assertThat("Image Content Length",(long)contentLength,is(pngFile.length()));
|
||||||
|
client.readBody(digesterImg,contentLength);
|
||||||
|
|
||||||
|
// Validate checksums
|
||||||
|
IO.close(gziperMain);
|
||||||
|
IO.close(digesterMain);
|
||||||
|
assertChecksum("lots-of-fantasy-names.txt",digestMain);
|
||||||
|
IO.close(digesterImg);
|
||||||
|
assertChecksum("jetty_logo.png",digestImg);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertChecksum(String testResourceFile, MessageDigest digest) throws IOException
|
||||||
|
{
|
||||||
|
String expectedSha1 = loadSha1sum(testResourceFile + ".sha1");
|
||||||
|
String actualSha1 = Hex.asHex(digest.digest());
|
||||||
|
Assert.assertEquals(testResourceFile + " / SHA1Sum of content",expectedSha1,actualSha1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadSha1sum(String testResourceSha1Sum) throws IOException
|
||||||
|
{
|
||||||
|
File sha1File = MavenTestingUtils.getTestResourceFile(testResourceSha1Sum);
|
||||||
|
String contents = IO.readToString(sha1File);
|
||||||
|
Pattern pat = Pattern.compile("^[0-9A-Fa-f]*");
|
||||||
|
Matcher mat = pat.matcher(contents);
|
||||||
|
Assert.assertTrue("Should have found HEX code in SHA1 file: " + sha1File,mat.find());
|
||||||
|
return mat.group();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
package org.eclipse.jetty.servlets;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.util.log.Log;
|
||||||
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
import org.eclipse.jetty.util.log.StdErrLog;
|
||||||
|
import org.junit.Assert;
|
||||||
|
|
||||||
|
public class PipelineHelper
|
||||||
|
{
|
||||||
|
private static final Logger LOG = Log.getLogger(PipelineHelper.class);
|
||||||
|
private URI uri;
|
||||||
|
private SocketAddress endpoint;
|
||||||
|
private Socket socket;
|
||||||
|
private OutputStream outputStream;
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
|
public PipelineHelper(URI uri)
|
||||||
|
{
|
||||||
|
if (LOG instanceof StdErrLog)
|
||||||
|
{
|
||||||
|
((StdErrLog)LOG).setLevel(StdErrLog.LEVEL_DEBUG);
|
||||||
|
}
|
||||||
|
this.uri = uri;
|
||||||
|
this.endpoint = new InetSocketAddress(uri.getHost(),uri.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the Socket to the destination endpoint and
|
||||||
|
*
|
||||||
|
* @return the open java Socket.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public Socket connect() throws IOException
|
||||||
|
{
|
||||||
|
LOG.info("Connecting to endpoint: " + endpoint);
|
||||||
|
socket = new Socket();
|
||||||
|
socket.setTcpNoDelay(true);
|
||||||
|
socket.connect(endpoint,1000);
|
||||||
|
|
||||||
|
outputStream = socket.getOutputStream();
|
||||||
|
inputStream = socket.getInputStream();
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue a HTTP/1.1 GET request with Connection:keep-alive set.
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* the path to GET
|
||||||
|
* @param acceptGzipped
|
||||||
|
* to turn on acceptance of GZIP compressed responses
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public void issueGET(String path, boolean acceptGzipped) throws IOException
|
||||||
|
{
|
||||||
|
LOG.debug("Issuing GET on " + path);
|
||||||
|
StringBuilder req = new StringBuilder();
|
||||||
|
req.append("GET ").append(uri.resolve(path).getPath()).append(" HTTP/1.1\r\n");
|
||||||
|
req.append("Host: ").append(uri.getHost()).append(":").append(uri.getPort()).append("\r\n");
|
||||||
|
req.append("User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3\r\n");
|
||||||
|
req.append("Accept: */*\r\n");
|
||||||
|
req.append("Referer: http://mycompany.com/index.html\r\n");
|
||||||
|
req.append("Accept-Language: en-us\r\n");
|
||||||
|
if (acceptGzipped)
|
||||||
|
{
|
||||||
|
req.append("Accept-Encoding: gzip, deflate\r\n");
|
||||||
|
}
|
||||||
|
req.append("Cookie: JSESSIONID=spqx8v8szylt1336t96vc6mw0\r\n");
|
||||||
|
req.append("Connection: keep-alive\r\n");
|
||||||
|
req.append("\r\n");
|
||||||
|
|
||||||
|
LOG.debug("Request:" + req);
|
||||||
|
|
||||||
|
// Send HTTP GET Request
|
||||||
|
byte buf[] = req.toString().getBytes();
|
||||||
|
outputStream.write(buf,0,buf.length);
|
||||||
|
outputStream.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readResponseHeader() throws IOException
|
||||||
|
{
|
||||||
|
// Read Response Header
|
||||||
|
socket.setSoTimeout(10000);
|
||||||
|
|
||||||
|
LOG.debug("Reading http header");
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
boolean foundEnd = false;
|
||||||
|
String line;
|
||||||
|
while (!foundEnd)
|
||||||
|
{
|
||||||
|
line = readLine();
|
||||||
|
// System.out.printf("RESP: \"%s\"%n",line);
|
||||||
|
if (line.length() == 0)
|
||||||
|
{
|
||||||
|
foundEnd = true;
|
||||||
|
LOG.debug("Got full http response header");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.append(line).append("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readLine() throws IOException
|
||||||
|
{
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
boolean foundCR = false;
|
||||||
|
boolean foundLF = false;
|
||||||
|
int b;
|
||||||
|
while (!(foundCR && foundLF))
|
||||||
|
{
|
||||||
|
b = inputStream.read();
|
||||||
|
Assert.assertThat("Should not have hit EOL (yet) during chunk size read",(int)b,not(-1));
|
||||||
|
if (b == 0x0D)
|
||||||
|
{
|
||||||
|
foundCR = true;
|
||||||
|
}
|
||||||
|
else if (b == 0x0A)
|
||||||
|
{
|
||||||
|
foundLF = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foundCR = false;
|
||||||
|
foundLF = false;
|
||||||
|
line.append((char)b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long readChunkSize() throws IOException
|
||||||
|
{
|
||||||
|
StringBuilder chunkSize = new StringBuilder();
|
||||||
|
String validHex = "0123456789ABCDEF";
|
||||||
|
boolean foundCR = false;
|
||||||
|
boolean foundLF = false;
|
||||||
|
int b;
|
||||||
|
while (!(foundCR && foundLF))
|
||||||
|
{
|
||||||
|
b = inputStream.read();
|
||||||
|
Assert.assertThat("Should not have hit EOL (yet) during chunk size read",(int)b,not(-1));
|
||||||
|
if (b == 0x0D)
|
||||||
|
{
|
||||||
|
foundCR = true;
|
||||||
|
}
|
||||||
|
else if (b == 0x0A)
|
||||||
|
{
|
||||||
|
foundLF = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foundCR = false;
|
||||||
|
foundLF = false;
|
||||||
|
// Must be valid char
|
||||||
|
char c = (char)b;
|
||||||
|
if (validHex.indexOf(c) >= 0)
|
||||||
|
{
|
||||||
|
chunkSize.append(c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.fail(String.format("Encountered invalid chunk size byte 0x%X",b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Long.parseLong(chunkSize.toString(),16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readBody(OutputStream stream, int size) throws IOException
|
||||||
|
{
|
||||||
|
int left = size;
|
||||||
|
while (left > 0)
|
||||||
|
{
|
||||||
|
int val = inputStream.read();
|
||||||
|
if (val == (-1))
|
||||||
|
{
|
||||||
|
Assert.fail(String.format("Encountered an early EOL (expected another %,d bytes)",left));
|
||||||
|
}
|
||||||
|
stream.write(val);
|
||||||
|
left--;
|
||||||
|
}
|
||||||
|
return size - left;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] readResponseBody(int size) throws IOException
|
||||||
|
{
|
||||||
|
byte partial[] = new byte[size];
|
||||||
|
int readBytes = 0;
|
||||||
|
int bytesLeft = size;
|
||||||
|
while (readBytes < size)
|
||||||
|
{
|
||||||
|
int len = inputStream.read(partial,readBytes,bytesLeft);
|
||||||
|
Assert.assertThat("Read should not have hit EOL yet",len,not(-1));
|
||||||
|
System.out.printf("Read %,d bytes%n",len);
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
readBytes += len;
|
||||||
|
bytesLeft -= len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return partial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputStream getOutputStream()
|
||||||
|
{
|
||||||
|
return outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream()
|
||||||
|
{
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SocketAddress getEndpoint()
|
||||||
|
{
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Socket getSocket()
|
||||||
|
{
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnect() throws IOException
|
||||||
|
{
|
||||||
|
LOG.debug("disconnect");
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getContentLength(String respHeader)
|
||||||
|
{
|
||||||
|
Pattern pat = Pattern.compile("Content-Length: ([0-9]*)",Pattern.CASE_INSENSITIVE);
|
||||||
|
Matcher mat = pat.matcher(respHeader);
|
||||||
|
if (mat.find())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Integer.parseInt(mat.group(1));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Undefined content length
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
b49b039adf40b695217e6e369513767a7c1e7dc6 lots-of-fantasy-names.txt
|
Loading…
Reference in New Issue