package org.eclipse.jetty.servlets.gzip; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.util.Enumeration; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import javax.servlet.Servlet; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.io.ByteArrayBuffer; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.GzipFilter; import org.eclipse.jetty.testing.HttpTester; import org.eclipse.jetty.testing.ServletTester; import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.TestingDir; import org.junit.Assert; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class GzipTester { private Class<? extends GzipFilter> gzipFilterClass = GzipFilter.class; private String encoding = "ISO8859_1"; private String userAgent = null; private ServletTester servletTester; private TestingDir testdir; public GzipTester(TestingDir testingdir) { this.testdir = testingdir; // Make sure we start with a clean testing directory. // DOES NOT WORK IN WINDOWS - this.testdir.ensureEmpty(); } public void assertIsResponseGzipCompressed(String filename) throws Exception { assertIsResponseGzipCompressed(filename,filename); } public void assertIsResponseGzipCompressed(String requestedFilename, String serverFilename) throws Exception { System.err.printf("[GzipTester] requesting /context/%s%n",requestedFilename); HttpTester request = new HttpTester(); HttpTester response = new HttpTester(); request.setMethod("GET"); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding","gzip"); if (this.userAgent != null) request.setHeader("User-Agent", this.userAgent); request.setURI("/context/" + requestedFilename); // Issue the request ByteArrayBuffer reqsBuff = new ByteArrayBuffer(request.generate().getBytes()); // Collect the response(s) ByteArrayBuffer respBuff = servletTester.getResponses(reqsBuff); response.parse(respBuff.asArray()); // Assert the response headers Assert.assertThat("Response.method",response.getMethod(),nullValue()); Assert.assertThat("Response.status",response.getStatus(),is(HttpServletResponse.SC_OK)); Assert.assertThat("Response.header[Content-Length]",response.getHeader("Content-Length"),notNullValue()); Assert.assertThat("Response.header[Content-Encoding]",response.getHeader("Content-Encoding"),containsString("gzip")); // Assert that the decompressed contents are what we expect. File serverFile = testdir.getFile(serverFilename); String expected = IO.readToString(serverFile); String actual = null; ByteArrayInputStream bais = null; InputStream in = null; ByteArrayOutputStream out = null; try { bais = new ByteArrayInputStream(response.getContentBytes()); in = new GZIPInputStream(bais); out = new ByteArrayOutputStream(); IO.copy(in,out); actual = out.toString(encoding); Assert.assertEquals("Uncompressed contents",expected,actual); } finally { IO.close(out); IO.close(in); IO.close(bais); } } /** * Makes sure that the response contains an unfiltered file contents. * <p> * This is used to test exclusions and passthroughs in the GzipFilter. * <p> * An example is to test that it is possible to configure GzipFilter to not recompress content that shouldn't be * compressed by the GzipFilter. * * @param requestedFilename * the filename used to on the GET request,. * @param testResourceSha1Sum * the sha1sum file that contains the SHA1SUM checksum that will be used to verify that the response * contents are what is intended. * @param expectedContentType */ public void assertIsResponseNotGzipFiltered(String requestedFilename, String testResourceSha1Sum, String expectedContentType) throws Exception { System.err.printf("[GzipTester] requesting /context/%s%n",requestedFilename); HttpTester request = new HttpTester(); HttpTester response = new HttpTester(); request.setMethod("GET"); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding","gzip"); if (this.userAgent != null) request.setHeader("User-Agent", this.userAgent); request.setURI("/context/" + requestedFilename); // Issue the request ByteArrayBuffer reqsBuff = new ByteArrayBuffer(request.generate().getBytes()); // Collect the response(s) ByteArrayBuffer respBuff = servletTester.getResponses(reqsBuff); response.parse(respBuff.asArray()); dumpHeaders(requestedFilename + " / Response Headers",response); // Assert the response headers String prefix = requestedFilename + " / Response"; Assert.assertThat(prefix + ".method",response.getMethod(),nullValue()); Assert.assertThat(prefix + ".status",response.getStatus(),is(HttpServletResponse.SC_OK)); Assert.assertThat(prefix + ".header[Content-Length]",response.getHeader("Content-Length"),notNullValue()); Assert.assertThat(prefix + ".header[Content-Encoding] (should not be recompressed by GzipFilter)",response.getHeader("Content-Encoding"),nullValue()); Assert.assertThat(prefix + ".header[Content-Type] (should have a Content-Type associated with it)",response.getHeader("Content-Type"),notNullValue()); Assert.assertThat(prefix + ".header[Content-Type]",response.getHeader("Content-Type"),is(expectedContentType)); ByteArrayInputStream bais = null; DigestOutputStream digester = null; try { MessageDigest digest = MessageDigest.getInstance("SHA1"); bais = new ByteArrayInputStream(response.getContentBytes()); digester = new DigestOutputStream(new NoOpOutputStream(),digest); IO.copy(bais,digester); String actualSha1Sum = Hex.asHex(digest.digest()); String expectedSha1Sum = loadExpectedSha1Sum(testResourceSha1Sum); Assert.assertEquals(requestedFilename + " / SHA1Sum of content",expectedSha1Sum,actualSha1Sum); } finally { IO.close(digester); IO.close(bais); } } private void dumpHeaders(String prefix, HttpTester http) { System.out.println(prefix); @SuppressWarnings("unchecked") Enumeration<String> names = http.getHeaderNames(); while (names.hasMoreElements()) { String name = names.nextElement(); String value = http.getHeader(name); System.out.printf(" [%s] = %s%n",name,value); } } private String loadExpectedSha1Sum(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(); } /** * Asserts that the requested filename results in a properly structured GzipFilter response, where the content is * not compressed, and the content-length is returned appropriately. * * @param filename * the filename used for the request, and also used to compare the response to the server file, assumes * that the file is suitable for {@link Assert#assertEquals(Object, Object)} use. (in other words, the * contents of the file are text) * @param expectedFilesize * the expected filesize to be specified on the Content-Length portion of the response headers. (note: * passing -1 will disable the Content-Length assertion) * @throws Exception */ public void assertIsResponseNotGzipCompressed(String filename, int expectedFilesize, int status) throws Exception { System.err.printf("[GzipTester] requesting /context/%s%n",filename); HttpTester request = new HttpTester(); HttpTester response = new HttpTester(); request.setMethod("GET"); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding","gzip"); if (this.userAgent != null) request.setHeader("User-Agent", this.userAgent); if (filename == null) request.setURI("/context/"); else request.setURI("/context/"+filename); // Issue the request ByteArrayBuffer reqsBuff = new ByteArrayBuffer(request.generate().getBytes()); // Collect the response(s) ByteArrayBuffer respBuff = servletTester.getResponses(reqsBuff); response.parse(respBuff.asArray()); // Assert the response headers Assert.assertThat("Response.method",response.getMethod(),nullValue()); Assert.assertThat("Response.status",response.getStatus(),is(status)); if (expectedFilesize != (-1)) { Assert.assertThat("Response.header[Content-Length]",response.getHeader("Content-Length"),notNullValue()); int serverLength = Integer.parseInt(response.getHeader("Content-Length")); Assert.assertThat("Response.header[Content-Length]",serverLength,is(expectedFilesize)); } Assert.assertThat("Response.header[Content-Encoding]",response.getHeader("Content-Encoding"),not(containsString("gzip"))); // Assert that the contents are what we expect. if (filename != null) { File serverFile = testdir.getFile(filename); String expected = IO.readToString(serverFile); String actual = null; InputStream in = null; ByteArrayOutputStream out = null; try { in = new ByteArrayInputStream(response.getContentBytes()); out = new ByteArrayOutputStream(); IO.copy(in,out); actual = out.toString(encoding); Assert.assertEquals("Server contents",expected,actual); } finally { IO.close(out); IO.close(in); } } } /** * Generate string content of arbitrary length. * * @param length * the length of the string to generate. * @return the string content. */ public String generateContent(int length) { StringBuilder builder = new StringBuilder(); do { builder.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.\n"); builder.append("Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque\n"); builder.append("habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n"); builder.append("Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam\n"); builder.append("at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate\n"); builder.append("velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.\n"); builder.append("Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum\n"); builder.append("eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa\n"); builder.append("sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam\n"); builder.append("consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.\n"); builder.append("Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse\n"); builder.append("et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.\n"); } while (builder.length() < length); // Make sure we are exactly at requested length. (truncate the extra) if (builder.length() > length) { builder.setLength(length); } return builder.toString(); } public String getEncoding() { return encoding; } /** * Create a file on the server resource path of a specified filename and size. * * @param filename * the filename to create * @param filesize * the file size to create (Note: this isn't suitable for creating large multi-megabyte files) */ public File prepareServerFile(String filename, int filesize) throws IOException { File dir = testdir.getDir(); File testFile = new File(dir,filename); // Make sure we have a uniq filename (to work around windows File.delete bug) int i = 0; while (testFile.exists()) { testFile = new File(dir,(i++) + "-" + filename); } FileOutputStream fos = null; ByteArrayInputStream in = null; try { fos = new FileOutputStream(testFile,false); in = new ByteArrayInputStream(generateContent(filesize).getBytes(encoding)); IO.copy(in,fos); return testFile; } finally { IO.close(in); IO.close(fos); } } /** * Copy a src/test/resource file into the server tree for eventual serving. * * @param filename * the filename to look for in src/test/resources */ public void copyTestServerFile(String filename) throws IOException { File srcFile = MavenTestingUtils.getTestResourceFile(filename); File testFile = testdir.getFile(filename); IO.copy(srcFile,testFile); } /** * Set the servlet that provides content for the GzipFilter in being tested. * * @param servletClass * the servlet that will provide content. * @return the FilterHolder for configuring the GzipFilter's initParameters with */ public FilterHolder setContentServlet(Class<? extends Servlet> servletClass) throws IOException { servletTester = new ServletTester(); servletTester.setContextPath("/context"); servletTester.setResourceBase(testdir.getDir().getCanonicalPath()); ServletHolder servletHolder = servletTester.addServlet(servletClass,"/"); servletHolder.setInitParameter("baseDir",testdir.getDir().getAbsolutePath()); FilterHolder holder = servletTester.addFilter(gzipFilterClass,"/*",0); return holder; } public Class<? extends GzipFilter> getGzipFilterClass() { return gzipFilterClass; } public void setGzipFilterClass(Class<? extends GzipFilter> gzipFilterClass) { this.gzipFilterClass = gzipFilterClass; } public void setEncoding(String encoding) { this.encoding = encoding; } public void setUserAgent(String ua) { this.userAgent = ua; } public void start() throws Exception { Assert.assertThat("No servlet defined yet. Did you use #setContentServlet()?",servletTester,notNullValue()); servletTester.dump(); servletTester.start(); } public void stop() { // NOTE: Do not cleanup the testdir. Failures can't be diagnosed if you do that. // IO.delete(testdir.getDir()): try { servletTester.stop(); } catch (Exception e) { // Don't toss this out into Junit as this would be the last exception // that junit will report as being the cause of the test failure. // when in reality, the earlier setup issue is the real cause. e.printStackTrace(System.err); } } }