From be21c35bf807374e2b2db4449541fc194579c656 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 21 Aug 2018 15:23:54 +0200 Subject: [PATCH] implemented WebResourceServlet, which loads resources from the UberWebResourceLoader --- .../sonia/scm/resources/ResourceManager.java | 4 + .../java/sonia/scm/WebResourceServlet.java | 77 ++++++++++++ .../plugin/DefaultUberWebResourceLoader.java | 3 +- .../resources/AbstractResourceManager.java | 10 +- .../sonia/scm/WebResourceServletTest.java | 115 ++++++++++++++++++ 5 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java create mode 100644 scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java diff --git a/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java b/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java index c287b8a7eb..8fa9187ad7 100644 --- a/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java +++ b/scm-core/src/main/java/sonia/scm/resources/ResourceManager.java @@ -40,9 +40,13 @@ import java.util.List; * This class collects and manages {@link Resource} * which are used by the web interface. * + * TODO remove before 2.0.0 + * * @author Sebastian Sdorra * @since 1.12 + * @deprecated unnecessary for new ui */ +@Deprecated public interface ResourceManager { diff --git a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java new file mode 100644 index 0000000000..a2f96827e6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java @@ -0,0 +1,77 @@ +package sonia.scm; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.io.Resources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.filter.WebElement; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.UberWebResourceLoader; +import sonia.scm.util.HttpUtil; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; + +/** + * WebResourceServlet serves resources from the {@link UberWebResourceLoader}. + * + * @since 2.0.0 + */ +@Singleton +@WebElement(value = WebResourceServlet.PATTERN, regex = true) +public class WebResourceServlet extends HttpServlet { + + /** + * exclude api requests and the old frontend servlets. + * + * TODO remove old frontend servlets + */ + @VisibleForTesting + static final String PATTERN = "/(?!api/|index.html|error.html|plugins/resources).+"; + + private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class); + + private final UberWebResourceLoader webResourceLoader; + + @Inject + public WebResourceServlet(PluginLoader pluginLoader) { + this.webResourceLoader = pluginLoader.getUberWebResourceLoader(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + String uri = normalizeUri(request); + + LOG.trace("try to load {}", uri); + URL url = webResourceLoader.getResource(uri); + if (url != null) { + serveResource(response, url); + } else { + handleResourceNotFound(response); + } + } + + private String normalizeUri(HttpServletRequest request) { + return HttpUtil.getStrippedURI(request); + } + + private void serveResource(HttpServletResponse response, URL url) { + // TODO lastModifiedDate, if-... ??? + try (OutputStream output = response.getOutputStream()) { + Resources.copy(url, output); + } catch (IOException ex) { + LOG.warn("failed to serve resource: {}", url); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + private void handleResourceNotFound(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java index 3bc39edb98..c8157925ef 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultUberWebResourceLoader.java @@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableList.Builder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.util.HttpUtil; //~--- JDK imports ------------------------------------------------------------ @@ -185,7 +186,7 @@ public class DefaultUberWebResourceLoader implements UberWebResourceLoader */ private URL find(String path) { - URL resource = null; + URL resource; try { diff --git a/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java b/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java index 95d7b39b69..4772acdf0c 100644 --- a/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java +++ b/scm-webapp/src/main/java/sonia/scm/resources/AbstractResourceManager.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import javax.servlet.ServletContext; import sonia.scm.plugin.PluginWrapper; @@ -146,7 +147,7 @@ public abstract class AbstractResourceManager implements ResourceManager processPlugin(resources, plugin.getPlugin()); } } - + // fix order of script resources, see https://goo.gl/ok03l4 Collections.sort(resources); @@ -172,7 +173,12 @@ public abstract class AbstractResourceManager implements ResourceManager if (scriptResources != null) { - resources.addAll(scriptResources); + // filter new resources and keep only the old ones, which are starting with a / + List filtered = scriptResources.stream() + .filter((res) -> res.startsWith("/")) + .collect(Collectors.toList()); + + resources.addAll(filtered); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java new file mode 100644 index 0000000000..af17fda77d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java @@ -0,0 +1,115 @@ +package sonia.scm; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.UberWebResourceLoader; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class WebResourceServletTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private PluginLoader pluginLoader; + + @Mock + private UberWebResourceLoader webResourceLoader; + + private WebResourceServlet servlet; + + @Before + public void setUpMocks() { + when(pluginLoader.getUberWebResourceLoader()).thenReturn(webResourceLoader); + when(request.getContextPath()).thenReturn("/scm"); + servlet = new WebResourceServlet(pluginLoader); + } + + @Test + public void testPattern() { + assertTrue("/some/resource".matches(WebResourceServlet.PATTERN)); + assertTrue("/favicon.ico".matches(WebResourceServlet.PATTERN)); + assertTrue("/other.html".matches(WebResourceServlet.PATTERN)); + assertFalse("/api/v2/repositories".matches(WebResourceServlet.PATTERN)); + + // exclude old style ui template servlets + assertFalse("/".matches(WebResourceServlet.PATTERN)); + assertFalse("/index.html".matches(WebResourceServlet.PATTERN)); + assertFalse("/error.html".matches(WebResourceServlet.PATTERN)); + assertFalse("/plugins/resources/js/sonia/scm/hg.config-wizard.js".matches(WebResourceServlet.PATTERN)); + } + + @Test + public void testDoGetWithNonExistingResource() { + when(request.getRequestURI()).thenReturn("/scm/awesome.jpg"); + servlet.doGet(request, response); + verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND); + } + + + @Test + public void testDoGet() throws IOException { + when(request.getRequestURI()).thenReturn("/scm/README.txt"); + TestingOutputServletOutputStream output = new TestingOutputServletOutputStream(); + when(response.getOutputStream()).thenReturn(output); + + File file = temporaryFolder.newFile(); + Files.write("hello".getBytes(Charsets.UTF_8), file); + + when(webResourceLoader.getResource("/README.txt")).thenReturn(file.toURI().toURL()); + servlet.doGet(request, response); + + assertEquals("hello", output.buffer.toString()); + } + + @Test + public void testDoGetWithError() throws IOException { + when(request.getRequestURI()).thenReturn("/scm/README.txt"); + ServletOutputStream output = mock(ServletOutputStream.class); + when(response.getOutputStream()).thenReturn(output); + + File file = temporaryFolder.newFile(); + assertTrue(file.delete()); + + when(webResourceLoader.getResource("/README.txt")).thenReturn(file.toURI().toURL()); + servlet.doGet(request, response); + + verify(output, never()).write(anyInt()); + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + private static class TestingOutputServletOutputStream extends ServletOutputStream { + + private ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + @Override + public void write(int b) { + buffer.write(b); + } + } + +}