// // ======================================================================== // Copyright (c) 1995-2017 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.server.handler.gzip; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; 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.io.UnsupportedEncodingException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.util.EnumSet; import java.util.Enumeration; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import javax.servlet.DispatcherType; import javax.servlet.Servlet; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.DateGenerator; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester.Response; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletTester; import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.TestingDir; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.hamcrest.Matchers; import org.junit.Assert; public class GzipTester { private static final Logger LOG = Log.getLogger(GzipTester.class); public static class ContentMetadata { public final long size; public final String sha1; public ContentMetadata(long size, String sha1checksum) { this.size = size; this.sha1 = sha1checksum; } } private String encoding = "ISO8859_1"; private String userAgent = null; private final ServletTester tester = new ServletTester("/context",ServletContextHandler.GZIP); private TestingDir testdir; private String accept; private String compressionType; public GzipTester(TestingDir testingdir, String compressionType, String accept) { this.testdir = testingdir; this.compressionType = compressionType; this.accept = accept; } public GzipTester(TestingDir testingdir, String compressionType) { this.testdir = testingdir; this.compressionType = compressionType; this.accept = compressionType; } public GzipHandler getGzipHandler() { return tester.getContext().getGzipHandler(); } public int getOutputBufferSize() { return tester.getConnector().getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().getOutputBufferSize(); } public ContentMetadata getResponseMetadata(Response response) throws Exception { long size = response.getContentBytes().length; String contentEncoding = response.get("Content-Encoding"); ByteArrayInputStream bais = null; InputStream in = null; DigestOutputStream digester = null; ByteArrayOutputStream uncompressedStream = null; try { MessageDigest digest = MessageDigest.getInstance("SHA1"); bais = new ByteArrayInputStream(response.getContentBytes()); if (contentEncoding == null) { LOG.debug("No response content-encoding"); in = new PassThruInputStream(bais); } else if (contentEncoding.contains(GzipHandler.GZIP)) { in = new GZIPInputStream(bais); } else if (contentEncoding.contains(GzipHandler.DEFLATE)) { in = new InflaterInputStream(bais,new Inflater(true)); } else { assertThat("Unexpected response content-encoding", contentEncoding, isEmptyOrNullString()); } uncompressedStream = new ByteArrayOutputStream((int)size); digester = new DigestOutputStream(uncompressedStream,digest); IO.copy(in,digester); byte output[] = uncompressedStream.toByteArray(); String actualSha1Sum = Hex.asHex(digest.digest()); return new ContentMetadata(output.length,actualSha1Sum); } finally { IO.close(digester); IO.close(in); IO.close(bais); IO.close(uncompressedStream); } } public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename) throws Exception { return assertIsResponseGzipCompressed(method,filename,filename,-1); } public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename, long ifmodifiedsince) throws Exception { return assertIsResponseGzipCompressed(method,filename,filename,ifmodifiedsince); } public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename) throws Exception { return assertIsResponseGzipCompressed(method,requestedFilename,serverFilename,-1); } public HttpTester.Response assertNonStaticContentIsResponseGzipCompressed(String method, String path, String expected) throws Exception { HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; request.setMethod(method); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",accept); if (this.userAgent != null) request.setHeader("User-Agent",this.userAgent); request.setURI("/context/" + path); // Issue the request response = HttpTester.parseResponse(tester.getResponses(request.generate())); int qindex = compressionType.indexOf(";"); if (qindex < 0) Assert.assertThat("Response.header[Content-Encoding]",response.get("Content-Encoding"),containsString(compressionType)); else Assert.assertThat("Response.header[Content-Encoding]",response.get("Content-Encoding"),containsString(compressionType.substring(0,qindex))); ByteArrayInputStream bais = null; InputStream in = null; ByteArrayOutputStream out = null; String actual = null; try { bais = new ByteArrayInputStream(response.getContentBytes()); if (compressionType.startsWith(GzipHandler.GZIP)) { in = new GZIPInputStream(bais); } else if (compressionType.startsWith(GzipHandler.DEFLATE)) { in = new InflaterInputStream(bais,new Inflater(true)); } out = new ByteArrayOutputStream(); IO.copy(in,out); actual = out.toString(encoding); assertThat("Uncompressed contents",actual,equalTo(expected)); } finally { IO.close(out); IO.close(in); IO.close(bais); } return response; } public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename, long ifmodifiedsince) throws Exception { HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; request.setMethod(method); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",compressionType); if (ifmodifiedsince > 0) request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(),DateGenerator.formatDate(ifmodifiedsince)); if (this.userAgent != null) request.setHeader("User-Agent",this.userAgent); request.setURI("/context/" + requestedFilename); // Issue the request response = HttpTester.parseResponse(tester.getResponses(request.generate())); // Assert the response headers // Assert.assertThat("Response.status",response.getStatus(),is(HttpServletResponse.SC_OK)); // Response headers should have either a Transfer-Encoding indicating chunked OR a Content-Length /* * TODO need to check for the 3rd option of EOF content. To do this properly you might need to look at both HTTP/1.1 and HTTP/1.0 requests String * contentLength = response.get("Content-Length"); String transferEncoding = response.get("Transfer-Encoding"); * * boolean chunked = (transferEncoding != null) && (transferEncoding.indexOf("chunk") >= 0); if(!chunked) { * Assert.assertThat("Response.header[Content-Length]",contentLength,notNullValue()); } else { * Assert.assertThat("Response.header[Transfer-Encoding]",transferEncoding,notNullValue()); } */ int qindex = compressionType.indexOf(";"); if (qindex < 0) Assert.assertThat("Response.header[Content-Encoding]",response.get("Content-Encoding"),containsString(compressionType)); else Assert.assertThat("Response.header[Content-Encoding]",response.get("Content-Encoding"),containsString(compressionType.substring(0,qindex))); Assert.assertThat(response.get("ETag"),Matchers.startsWith("W/")); // Assert that the decompressed contents are what we expect. File serverFile = testdir.getPathFile(serverFilename).toFile(); String expected = IO.readToString(serverFile); String actual = null; ByteArrayInputStream bais = null; InputStream in = null; ByteArrayOutputStream out = null; try { bais = new ByteArrayInputStream(response.getContentBytes()); if (compressionType.startsWith(GzipHandler.GZIP)) { in = new GZIPInputStream(bais); } else if (compressionType.startsWith(GzipHandler.DEFLATE)) { in = new InflaterInputStream(bais,new Inflater(true)); } out = new ByteArrayOutputStream(); IO.copy(in,out); actual = out.toString(encoding); assertThat("Uncompressed contents",actual,equalTo(expected)); } finally { IO.close(out); IO.close(in); IO.close(bais); } return response; } public HttpTester.Response assertIsResponseNotModified(String method, String requestedFilename, long ifmodifiedsince) throws Exception { HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; request.setMethod(method); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",compressionType); if (ifmodifiedsince > 0) request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(),DateGenerator.formatDate(ifmodifiedsince)); if (this.userAgent != null) request.setHeader("User-Agent",this.userAgent); request.setURI("/context/" + requestedFilename); // Issue the request response = HttpTester.parseResponse(tester.getResponses(request.generate())); Assert.assertThat(response.getStatus(),Matchers.equalTo(304)); Assert.assertThat(response.get("ETag"),Matchers.startsWith("W/")); return response; } /** * Makes sure that the response contains an unfiltered file contents. * <p> * This is used to test exclusions and passthroughs in the GzipHandler. * <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 the expected content type * @throws Exception on test failure */ public void assertIsResponseNotGziped(String requestedFilename, String testResourceSha1Sum, String expectedContentType) throws Exception { assertIsResponseNotGzipFiltered(requestedFilename,testResourceSha1Sum,expectedContentType,null); } /** * Makes sure that the response contains an unfiltered file contents. * <p> * This is used to test exclusions and passthroughs in the GzipHandler. * <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 the expected content type * @param expectedContentEncoding * can be non-null in some circumstances, eg when dealing with pre-gzipped .svgz files * @throws Exception on test failure */ public void assertIsResponseNotGzipFiltered(String requestedFilename, String testResourceSha1Sum, String expectedContentType, String expectedContentEncoding) throws Exception { HttpTester.Request request = HttpTester.newRequest(); HttpTester.Response response; request.setMethod("GET"); request.setVersion("HTTP/1.0"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",compressionType); if (this.userAgent != null) request.setHeader("User-Agent",this.userAgent); request.setURI("/context/" + requestedFilename); // Issue the request response = HttpTester.parseResponse(tester.getResponses(request.generate())); dumpHeaders(requestedFilename + " / Response Headers",response); // Assert the response headers String prefix = requestedFilename + " / Response"; Assert.assertThat(prefix + ".status",response.getStatus(),is(HttpServletResponse.SC_OK)); Assert.assertThat(prefix + ".header[Content-Length]",response.get("Content-Length"),notNullValue()); Assert.assertThat(prefix + ".header[Content-Encoding] (should not be recompressed by GzipHandler)",response.get("Content-Encoding"), expectedContentEncoding == null?nullValue():notNullValue()); if (expectedContentEncoding != null) Assert.assertThat(prefix + ".header[Content-Encoding]",response.get("Content-Encoding"),is(expectedContentEncoding)); Assert.assertThat(prefix + ".header[Content-Type] (should have a Content-Type associated with it)",response.get("Content-Type"),notNullValue()); Assert.assertThat(prefix + ".header[Content-Type]",response.get("Content-Type"),is(expectedContentType)); Assert.assertThat(response.get("ETAG"),Matchers.startsWith("W/")); 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.Message message) { LOG.debug("dumpHeaders: {}",prefix); Enumeration<String> names = message.getFieldNames(); while (names.hasMoreElements()) { String name = names.nextElement(); String value = message.get(name); LOG.debug("dumpHeaders: {} = {}",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(); } public HttpTester.Response executeRequest(String method, String path, int idleFor, TimeUnit idleUnit) throws Exception { HttpTester.Request request = HttpTester.newRequest(); request.setMethod(method); request.setVersion("HTTP/1.1"); request.setHeader("Host","tester"); request.setHeader("Accept-Encoding",accept); request.setHeader("Connection","close"); if (this.userAgent != null) { request.setHeader("User-Agent",this.userAgent); } request.setURI(path); // Issue the request return HttpTester.parseResponse(tester.getResponses(request.generate(),idleFor,idleUnit)); } public String readResponse(HttpTester.Response response) throws IOException, UnsupportedEncodingException { String actual = null; InputStream in = null; ByteArrayOutputStream out = null; try { byte[] content = response.getContentBytes(); if (content != null) actual = new String(response.getContentBytes(),encoding); else actual = ""; } finally { IO.close(out); IO.close(in); } return actual; } /** * 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) * @return the prepared file * @throws IOException if unable to create file */ public File prepareServerFile(String filename, int filesize) throws IOException { File dir = testdir.getPath().toFile(); 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 * @throws IOException if unable to copy file */ public void copyTestServerFile(String filename) throws IOException { File srcFile = MavenTestingUtils.getTestResourceFile(filename); File testFile = testdir.getPathFile(filename).toFile(); IO.copy(srcFile,testFile); } /** * Set the servlet that provides content for the GzipHandler in being tested. * * @param servletClass * the servlet that will provide content. * @throws IOException if unable to set content servlet */ public void setContentServlet(Class<? extends Servlet> servletClass) throws IOException { String resourceBase = testdir.getPath().toString(); tester.setContextPath("/context"); tester.setResourceBase(resourceBase); ServletHolder servletHolder = tester.addServlet(servletClass,"/"); servletHolder.setInitParameter("baseDir",resourceBase); servletHolder.setInitParameter("etags","true"); } public void setEncoding(String encoding) { this.encoding = encoding; } public void setUserAgent(String ua) { this.userAgent = ua; } public void addMimeType(String extension, String mimetype) { this.tester.getContext().getMimeTypes().addMimeMapping(extension,mimetype); } /** * Add an arbitrary filter to the test case. * * @param holder * the filter to add * @param pathSpec * the path spec for this filter * @param dispatches * the set of {@link DispatcherType} to associate with this filter * @throws IOException if unable to add filter */ public void addFilter(FilterHolder holder, String pathSpec, EnumSet<DispatcherType> dispatches) throws IOException { tester.addFilter(holder,pathSpec,dispatches); } public void start() throws Exception { Assert.assertThat("No servlet defined yet. Did you use #setContentServlet()?",tester,notNullValue()); if (LOG.isDebugEnabled()) { tester.dumpStdErr(); } tester.start(); } public void stop() { // NOTE: Do not cleanup the testdir. Failures can't be diagnosed if you do that. // IO.delete(testdir.getDir()): try { tester.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); } } }