package io.dropwizard.servlets.assets; import com.google.common.net.HttpHeaders; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.servlet.ServletTester; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; public class AssetServletTest { private static final String DUMMY_SERVLET = "/dummy_servlet/"; private static final String NOINDEX_SERVLET = "/noindex_servlet/"; private static final String NOCHARSET_SERVLET = "/nocharset_servlet/"; private static final String ROOT_SERVLET = "/"; private static final String RESOURCE_PATH = "/assets"; // ServletTester expects to be able to instantiate the servlet with zero arguments public static class DummyAssetServlet extends AssetServlet { private static final long serialVersionUID = -1L; public DummyAssetServlet() { super(RESOURCE_PATH, DUMMY_SERVLET, "index.htm", StandardCharsets.UTF_8); } } public static class NoIndexAssetServlet extends AssetServlet { private static final long serialVersionUID = -1L; public NoIndexAssetServlet() { super(RESOURCE_PATH, DUMMY_SERVLET, null, StandardCharsets.UTF_8); } } public static class RootAssetServlet extends AssetServlet { private static final long serialVersionUID = 1L; public RootAssetServlet() { super("/", ROOT_SERVLET, null, StandardCharsets.UTF_8); } } public static class NoCharsetAssetServlet extends AssetServlet { private static final long serialVersionUID = 1L; public NoCharsetAssetServlet() { super(RESOURCE_PATH, NOCHARSET_SERVLET, null, null); } } private static final ServletTester SERVLET_TESTER = new ServletTester(); private final HttpTester.Request request = HttpTester.newRequest(); private HttpTester.Response response; @BeforeClass public static void startServletTester() throws Exception { SERVLET_TESTER.addServlet(DummyAssetServlet.class, DUMMY_SERVLET + '*'); SERVLET_TESTER.addServlet(NoIndexAssetServlet.class, NOINDEX_SERVLET + '*'); SERVLET_TESTER.addServlet(NoCharsetAssetServlet.class, NOCHARSET_SERVLET + '*'); SERVLET_TESTER.addServlet(RootAssetServlet.class, ROOT_SERVLET + '*'); SERVLET_TESTER.start(); SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("mp4", "video/mp4"); SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("m4a", "audio/mp4"); } @AfterClass public static void stopServletTester() throws Exception { SERVLET_TESTER.stop(); } @Before public void setUp() throws Exception { request.setMethod("GET"); request.setURI(DUMMY_SERVLET + "example.txt"); request.setVersion(HttpVersion.HTTP_1_0); } @Test public void servesFilesMappedToRoot() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .isEqualTo("HELLO THERE"); } @Test public void servesCharset() throws Exception { request.setURI(DUMMY_SERVLET + "example.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE))) .isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8); request.setURI(NOCHARSET_SERVLET + "example.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.get(HttpHeader.CONTENT_TYPE)) .isEqualTo(MimeTypes.Type.TEXT_PLAIN.toString()); } @Test public void servesFilesFromRootsWithSameName() throws Exception { request.setURI(DUMMY_SERVLET + "example2.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .isEqualTo("HELLO THERE 2"); } @Test public void servesFilesWithA200() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .isEqualTo("HELLO THERE"); } @Test public void throws404IfTheAssetIsMissing() throws Exception { request.setURI(DUMMY_SERVLET + "doesnotexist.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); } @Test public void consistentlyAssignsETags() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final String firstEtag = response.get(HttpHeaders.ETAG); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final String secondEtag = response.get(HttpHeaders.ETAG); assertThat(firstEtag) .isEqualTo("\"174a6dd7325e64c609eab14ab1d30b86\"") .isEqualTo(secondEtag); } @Test public void assignsDifferentETagsForDifferentFiles() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final String firstEtag = response.get(HttpHeaders.ETAG); request.setURI(DUMMY_SERVLET + "foo.bar"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final String secondEtag = response.get(HttpHeaders.ETAG); assertThat(firstEtag) .isEqualTo("\"174a6dd7325e64c609eab14ab1d30b86\""); assertThat(secondEtag) .isEqualTo("\"378521448e0a3893a209edcc686d91ce\""); } @Test public void supportsIfNoneMatchRequests() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final String correctEtag = response.get(HttpHeaders.ETAG); request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final int statusWithMatchingEtag = response.getStatus(); request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag + "FOO"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final int statusWithNonMatchingEtag = response.getStatus(); assertThat(statusWithMatchingEtag) .isEqualTo(304); assertThat(statusWithNonMatchingEtag) .isEqualTo(200); } @Test public void consistentlyAssignsLastModifiedTimes() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final long firstLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final long secondLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); assertThat(firstLastModifiedTime) .isEqualTo(secondLastModifiedTime); } @Test public void supportsByteRangeForMedia() throws Exception { request.setURI(ROOT_SERVLET + "assets/foo.mp4"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); request.setURI(ROOT_SERVLET + "assets/foo.m4a"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); } @Test public void supportsFullByteRange() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); request.setHeader(HttpHeaders.RANGE, "bytes=0-"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo("HELLO THERE"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 0-10/11"); } @Test public void supportsCentralByteRange() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); request.setHeader(HttpHeaders.RANGE, "bytes=4-8"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo("O THE"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 4-8/11"); assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("5"); } @Test public void supportsFinalByteRange() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); request.setHeader(HttpHeaders.RANGE, "bytes=10-10"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo("E"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 10-10/11"); assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("1"); request.setHeader(HttpHeaders.RANGE, "bytes=-1"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo("E"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 10-10/11"); assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("1"); } @Test public void rejectsInvalidByteRanges() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); request.setHeader(HttpHeaders.RANGE, "bytes=test"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(416); request.setHeader(HttpHeaders.RANGE, "bytes="); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(416); request.setHeader(HttpHeaders.RANGE, "bytes=1-infinity"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(416); request.setHeader(HttpHeaders.RANGE, "test"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(416); } @Test public void supportsMultipleByteRanges() throws Exception { request.setURI(ROOT_SERVLET + "assets/example.txt"); request.setHeader(HttpHeaders.RANGE, "bytes=0-0,-1"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo("HE"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 0-0,10-10/11"); assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("2"); request.setHeader(HttpHeaders.RANGE, "bytes=5-6,7-10"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); assertThat(response.getStatus()).isEqualTo(206); assertThat(response.getContent()).isEqualTo(" THERE"); assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( "bytes 5-6,7-10/11"); assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("6"); } @Test public void supportsIfRangeMatchRequests() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); final String correctEtag = response.get(HttpHeaders.ETAG); request.setHeader(HttpHeaders.RANGE, "bytes=10-10"); request.setHeader(HttpHeaders.IF_RANGE, correctEtag); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); final int statusWithMatchingEtag = response.getStatus(); request.setHeader(HttpHeaders.IF_RANGE, correctEtag + "FOO"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request .generate())); final int statusWithNonMatchingEtag = response.getStatus(); assertThat(statusWithMatchingEtag).isEqualTo(206); assertThat(statusWithNonMatchingEtag).isEqualTo(200); } @Test public void supportsIfModifiedSinceRequests() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final long lastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, lastModifiedTime); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final int statusWithMatchingLastModifiedTime = response.getStatus(); request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, lastModifiedTime - 100); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final int statusWithStaleLastModifiedTime = response.getStatus(); request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, lastModifiedTime + 100); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); final int statusWithRecentLastModifiedTime = response.getStatus(); assertThat(statusWithMatchingLastModifiedTime) .isEqualTo(304); assertThat(statusWithStaleLastModifiedTime) .isEqualTo(200); assertThat(statusWithRecentLastModifiedTime) .isEqualTo(304); } @Test public void guessesMimeTypes() throws Exception { response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE))) .isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8); } @Test public void defaultsToHtml() throws Exception { request.setURI(DUMMY_SERVLET + "foo.bar"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE))) .isEqualTo(MimeTypes.Type.TEXT_HTML_UTF_8); } @Test public void servesIndexFilesByDefault() throws Exception { // Root directory listing: request.setURI(DUMMY_SERVLET); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .contains("/assets Index File"); // Subdirectory listing: request.setURI(DUMMY_SERVLET + "some_directory"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .contains("/assets/some_directory Index File"); // Subdirectory listing with slash: request.setURI(DUMMY_SERVLET + "some_directory/"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); assertThat(response.getContent()) .contains("/assets/some_directory Index File"); } @Test public void throwsA404IfNoIndexFileIsDefined() throws Exception { // Root directory listing: request.setURI(NOINDEX_SERVLET + '/'); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); // Subdirectory listing: request.setURI(NOINDEX_SERVLET + "some_directory"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); // Subdirectory listing with slash: request.setURI(NOINDEX_SERVLET + "some_directory/"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); } @Test public void doesNotAllowOverridingUrls() throws Exception { request.setURI(DUMMY_SERVLET + "file:/etc/passwd"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); } @Test public void doesNotAllowOverridingPaths() throws Exception { request.setURI(DUMMY_SERVLET + "/etc/passwd"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(404); } @Test public void allowsEncodedAssetNames() throws Exception { request.setURI(DUMMY_SERVLET + "encoded%20example.txt"); response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); assertThat(response.getStatus()) .isEqualTo(200); } }