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:
Joakim Erdfelt 2012-02-16 14:08:21 -07:00
parent 566eec65ce
commit 766ff7cf19
4 changed files with 3453 additions and 0 deletions

View File

@ -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();
}
}

View File

@ -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

View File

@ -0,0 +1 @@
b49b039adf40b695217e6e369513767a7c1e7dc6 lots-of-fantasy-names.txt