/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.linecorp.armeria.server.http.file; import static com.linecorp.armeria.common.http.HttpSessionProtocols.HTTP; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Date; import java.util.zip.GZIPInputStream; import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.logging.LoggingService; import io.netty.handler.codec.DateFormatter; public class HttpFileServiceTest { private static final String baseResourceDir = HttpFileServiceTest.class.getPackage().getName().replace('.', '/') + '/'; private static final File tmpDir; private static final Server server; private static int httpPort; static { try { tmpDir = Files.createTempDirectory("armeria-test.").toFile(); } catch (Exception e) { throw new Error(e); } final ServerBuilder sb = new ServerBuilder(); try { sb.serviceUnder( "/fs/", HttpFileService.forFileSystem(tmpDir.toPath()).decorate(LoggingService::new)); sb.serviceUnder( "/compressed/", HttpFileServiceBuilder.forClassPath(baseResourceDir + "foo") .serveCompressedFiles(true) .build()); sb.serviceUnder( "/", HttpFileService.forClassPath(baseResourceDir + "foo") .orElse(HttpFileService.forClassPath(baseResourceDir + "bar")) .decorate(LoggingService::new)); } catch (Exception e) { throw new Error(e); } server = sb.build(); } @BeforeClass public static void init() throws Exception { server.start().get(); httpPort = server.activePorts().values().stream() .filter(p -> p.protocol() == HTTP).findAny().get().localAddress().getPort(); } @AfterClass public static void destroy() throws Exception { server.stop(); // Delete the temporary files created for testing against the real file system. Files.walkFileTree(tmpDir.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } @Test public void testClassPathGet() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { final String lastModified; try (CloseableHttpResponse res = hc.execute(new HttpGet(newUri("/foo.txt")))) { lastModified = assert200Ok(res, "text/plain", "foo"); } // Test if the 'If-Modified-Since' header works as expected. HttpUriRequest req = new HttpGet(newUri("/foo.txt")); req.setHeader(HttpHeaders.IF_MODIFIED_SINCE, currentHttpDate()); req.setHeader(HttpHeaders.CONNECTION, "close"); try (CloseableHttpResponse res = hc.execute(req)) { assert304NotModified(res, lastModified); } } } @Test public void testClassPathOrElseGet() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal(); CloseableHttpResponse res = hc.execute(new HttpGet(newUri("/bar.txt")))) { assert200Ok(res, "text/plain", "bar"); } } @Test public void testUnknownMediaType() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal(); CloseableHttpResponse res = hc.execute(new HttpGet(newUri("/bar.unknown")))) { final String lastModified = assert200Ok(res, null, "Unknown Media Type"); HttpUriRequest req = new HttpGet(newUri("/bar.unknown")); req.setHeader(HttpHeaders.IF_MODIFIED_SINCE, currentHttpDate()); req.setHeader(HttpHeaders.CONNECTION, "close"); try (CloseableHttpResponse resCached = hc.execute(req)) { assert304NotModified(resCached, lastModified); } } } @Test public void testGetPreCompressedSupportsNone() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { HttpGet request = new HttpGet(newUri("/compressed/foo.txt")); try (CloseableHttpResponse res = hc.execute(request)) { assertThat(res.getFirstHeader("Content-Encoding"), is(nullValue())); assertThat(res.getFirstHeader("Content-Type").getValue(), is("text/plain; charset=utf-8")); final byte[] content = ByteStreams.toByteArray(res.getEntity().getContent()); assertThat(new String(content, StandardCharsets.UTF_8), is("foo")); } } } @Test public void testGetPreCompressedSupportsGzip() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { HttpGet request = new HttpGet(newUri("/compressed/foo.txt")); request.setHeader("Accept-Encoding", "gzip"); try (CloseableHttpResponse res = hc.execute(request)) { assertThat(res.getFirstHeader("Content-Encoding").getValue(), is("gzip")); assertThat(res.getFirstHeader("Content-Type").getValue(), is("text/plain; charset=utf-8")); final byte[] content; try (GZIPInputStream unzipper = new GZIPInputStream(res.getEntity().getContent())) { content = ByteStreams.toByteArray(unzipper); } assertThat(new String(content, StandardCharsets.UTF_8), is("foo")); } } } @Test public void testGetPreCompressedSupportsBrotli() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { HttpGet request = new HttpGet(newUri("/compressed/foo.txt")); request.setHeader("Accept-Encoding", "br"); try (CloseableHttpResponse res = hc.execute(request)) { assertThat(res.getFirstHeader("Content-Encoding").getValue(), is("br")); assertThat(res.getFirstHeader("Content-Type").getValue(), is("text/plain; charset=utf-8")); // Test would be more readable and fun by decompressing like the gzip one, but since JDK doesn't // support brotli yet, just compare the compressed content to avoid adding a complex dependency. final byte[] content = ByteStreams.toByteArray(res.getEntity().getContent()); assertThat(content, is(Resources.toByteArray(Resources.getResource( baseResourceDir + "foo/foo.txt.br")))); } } } @Test public void testGetPreCompressedSupportsBothPrefersBrotli() throws Exception { try (CloseableHttpClient hc = HttpClients.createMinimal()) { HttpGet request = new HttpGet(newUri("/compressed/foo.txt")); request.setHeader("Accept-Encoding", "gzip, br"); try (CloseableHttpResponse res = hc.execute(request)) { assertThat(res.getFirstHeader("Content-Encoding").getValue(), is("br")); assertThat(res.getFirstHeader("Content-Type").getValue(), is("text/plain; charset=utf-8")); // Test would be more readable and fun by decompressing like the gzip one, but since JDK doesn't // support brotli yet, just compare the compressed content to avoid adding a complex dependency. final byte[] content = ByteStreams.toByteArray(res.getEntity().getContent()); assertThat(content, is(Resources.toByteArray(Resources.getResource( baseResourceDir + "foo/foo.txt.br")))); } } } @Test public void testFileSystemGet() throws Exception { final File barFile = new File(tmpDir, "bar.html"); final String expectedContentA = "<html/>"; final String expectedContentB = "<html><body/></html>"; Files.write(barFile.toPath(), expectedContentA.getBytes(StandardCharsets.UTF_8)); try (CloseableHttpClient hc = HttpClients.createMinimal()) { final String lastModified; HttpUriRequest req = new HttpGet(newUri("/fs/bar.html")); try (CloseableHttpResponse res = hc.execute(req)) { lastModified = assert200Ok(res, "text/html", expectedContentA); } // Test if the 'If-Modified-Since' header works as expected. req = new HttpGet(newUri("/fs/bar.html")); req.setHeader(HttpHeaders.IF_MODIFIED_SINCE, currentHttpDate()); try (CloseableHttpResponse res = hc.execute(req)) { assert304NotModified(res, lastModified); } // Test if the 'If-Modified-Since' header works as expected after the file is modified. req = new HttpGet(newUri("/fs/bar.html")); req.setHeader(HttpHeaders.IF_MODIFIED_SINCE, currentHttpDate()); // HTTP-date has no sub-second precision; wait until the current second changes. Thread.sleep(1000); Files.write(barFile.toPath(), expectedContentB.getBytes(StandardCharsets.UTF_8)); try (CloseableHttpResponse res = hc.execute(req)) { final String newLastModified = assert200Ok(res, "text/html", expectedContentB); // Ensure that the 'Last-Modified' header did not change. assertThat(newLastModified, is(not(lastModified))); } // Test if the cache detects the file removal correctly. final boolean deleted = barFile.delete(); assertThat(deleted, is(true)); req = new HttpGet(newUri("/fs/bar.html")); req.setHeader(HttpHeaders.IF_MODIFIED_SINCE, currentHttpDate()); req.setHeader(HttpHeaders.CONNECTION, "close"); try (CloseableHttpResponse res = hc.execute(req)) { assert404NotFound(res); } } } private static String assert200Ok( CloseableHttpResponse res, String expectedContentType, String expectedContent) throws Exception { assertStatusLine(res, "HTTP/1.1 200 OK"); // Ensure that the 'Last-Modified' header exists and is well-formed. final String lastModified; assertThat(res.containsHeader(HttpHeaders.LAST_MODIFIED), is(true)); lastModified = res.getFirstHeader(HttpHeaders.LAST_MODIFIED).getValue(); DateFormatter.parseHttpDate(lastModified); // Ensure the content and its type are correct. assertThat(EntityUtils.toString(res.getEntity()), is(expectedContent)); if (expectedContentType != null) { assertThat(res.containsHeader(HttpHeaders.CONTENT_TYPE), is(true)); assertThat(res.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue(), startsWith(expectedContentType)); } else { assertThat(res.containsHeader(HttpHeaders.CONTENT_TYPE), is(false)); } return lastModified; } private static void assert304NotModified( CloseableHttpResponse res, String expectedLastModified) { assertStatusLine(res, "HTTP/1.1 304 Not Modified"); // Ensure that the 'Last-Modified' header did not change. assertThat(res.getFirstHeader(HttpHeaders.LAST_MODIFIED).getValue(), is(expectedLastModified)); // Ensure that the content does not exist. assertThat(res.getEntity(), is(nullValue())); } private static void assert404NotFound(CloseableHttpResponse res) { assertStatusLine(res, "HTTP/1.1 404 Not Found"); // Ensure that the 'Last-Modified' header does not exist. assertThat(res.getFirstHeader(HttpHeaders.LAST_MODIFIED), is(nullValue())); } private static void assertStatusLine(CloseableHttpResponse res, String expectedStatusLine) { assertThat(res.getStatusLine().toString(), is(expectedStatusLine)); } private static String currentHttpDate() { return DateFormatter.format(new Date()); } private static String newUri(String path) { return "http://127.0.0.1:" + httpPort + path; } }