aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--it/it-tests/src/test/java/it/serverSystem/HttpHeadersTest.java31
-rw-r--r--it/it-tests/src/test/java/it/serverSystem/LogsTest.java4
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java98
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java190
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html14
-rw-r--r--server/sonar-web/public/index.html5
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/web.xml8
-rw-r--r--sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java6
10 files changed, 351 insertions, 10 deletions
diff --git a/it/it-tests/src/test/java/it/serverSystem/HttpHeadersTest.java b/it/it-tests/src/test/java/it/serverSystem/HttpHeadersTest.java
index 7f25eefddbe..359c53c9c3d 100644
--- a/it/it-tests/src/test/java/it/serverSystem/HttpHeadersTest.java
+++ b/it/it-tests/src/test/java/it/serverSystem/HttpHeadersTest.java
@@ -22,12 +22,17 @@ package it.serverSystem;
import com.google.common.base.Throwables;
import com.sonar.orchestrator.Orchestrator;
import it.Category4Suite;
+import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import okhttp3.CacheControl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
+import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
@@ -38,6 +43,13 @@ public class HttpHeadersTest {
@ClassRule
public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+ private static String JS_HASH;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ JS_HASH = getJsHash();
+ }
+
@Test
public void verify_headers_of_base_url() throws Exception {
Response response = call(orchestrator.getServer().getUrl() + "/");
@@ -78,7 +90,7 @@ public class HttpHeadersTest {
@Test
public void verify_headers_of_css() throws Exception {
- Response response = call(orchestrator.getServer().getUrl() + "/css/sonar.css");
+ Response response = call(orchestrator.getServer().getUrl() + "/css/sonar." + JS_HASH + ".css");
verifySecurityHeaders(response);
verifyContentType(response, "text/css");
@@ -87,7 +99,7 @@ public class HttpHeadersTest {
@Test
public void verify_headers_of_js() throws Exception {
- Response response = call(orchestrator.getServer().getUrl() + "/js/bundles/app.js");
+ Response response = call(orchestrator.getServer().getUrl() + "/js/app." + JS_HASH + ".js");
verifySecurityHeaders(response);
verifyContentType(response, "application/javascript");
@@ -135,7 +147,7 @@ public class HttpHeadersTest {
* SONAR-8247
*/
private static void verifySecurityHeaders(Response httpResponse) {
- assertThat(httpResponse.isSuccessful()).isTrue();
+ assertThat(httpResponse.isSuccessful()).as("Code is %s", httpResponse.code()).isTrue();
assertThat(httpResponse.headers().get("X-Frame-Options")).isEqualTo("SAMEORIGIN");
assertThat(httpResponse.headers().get("X-XSS-Protection")).isEqualTo("1; mode=block");
assertThat(httpResponse.headers().get("X-Content-Type-Options")).isEqualTo("nosniff");
@@ -159,4 +171,17 @@ public class HttpHeadersTest {
throw Throwables.propagate(e);
}
}
+
+ /**
+ * Every JS and CSS files contains a hash between the file name and the extension.
+ */
+ private static String getJsHash() throws IOException {
+ File cssFolder = new File(orchestrator.getServer().getHome(), "web/css");
+ Optional<Path> cssPath = Files.list(cssFolder.toPath()).map(Path::getFileName).findFirst();
+ if (cssPath.isPresent()) {
+ String fileName = cssPath.get().toFile().getName();
+ return fileName.replace("sonar.", "").replace(".css", "");
+ }
+ throw new IllegalStateException("sonar.css hasn't been found");
+ }
}
diff --git a/it/it-tests/src/test/java/it/serverSystem/LogsTest.java b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java
index 4252df014ba..7d232031966 100644
--- a/it/it-tests/src/test/java/it/serverSystem/LogsTest.java
+++ b/it/it-tests/src/test/java/it/serverSystem/LogsTest.java
@@ -64,10 +64,10 @@ public class LogsTest {
// log "-" for anonymous
sendHttpRequest(ItUtils.newWsClient(orchestrator), PATH);
assertThat(accessLogsFile()).isFile().exists();
- verifyLastAccessLogLine("-", PATH, 404);
+ verifyLastAccessLogLine("-", PATH, 200);
sendHttpRequest(ItUtils.newAdminWsClient(orchestrator), PATH);
- verifyLastAccessLogLine("admin", PATH, 404);
+ verifyLastAccessLogLine("admin", PATH, 200);
}
@Test
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 809e02c0643..4064e747a88 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -120,6 +120,7 @@ import org.sonar.server.platform.monitoring.PluginsMonitor;
import org.sonar.server.platform.monitoring.SettingsMonitor;
import org.sonar.server.platform.monitoring.SonarQubeMonitor;
import org.sonar.server.platform.monitoring.SystemMonitor;
+import org.sonar.server.platform.web.WebPagesFilter;
import org.sonar.server.platform.web.requestid.HttpRequestIdModule;
import org.sonar.server.platform.ws.ChangeLogLevelAction;
import org.sonar.server.platform.ws.DbMigrationStatusAction;
@@ -257,6 +258,7 @@ public class PlatformLevel4 extends PlatformLevel {
ServerWs.class,
BackendCleanup.class,
IndexDefinitions.class,
+ WebPagesFilter.class,
// batch
BatchWsModule.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
index c2e5af4c7cc..91cf5b83310 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
@@ -21,6 +21,7 @@ package org.sonar.server.platform.platformlevel;
import org.sonar.server.organization.NoopDefaultOrganizationCache;
import org.sonar.server.platform.ServerImpl;
+import org.sonar.server.platform.web.WebPagesFilter;
import org.sonar.server.platform.ws.DbMigrationStatusAction;
import org.sonar.server.platform.ws.MigrateDbAction;
import org.sonar.server.platform.ws.StatusAction;
@@ -38,6 +39,8 @@ public class PlatformLevelSafeMode extends PlatformLevel {
protected void configureLevel() {
add(
ServerImpl.class,
+ WebPagesFilter.class,
+
// Server WS
StatusAction.class,
MigrateDbAction.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
new file mode 100644
index 00000000000..53e69d4759a
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.platform.web;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.web.ServletFilter;
+
+import static java.util.Locale.ENGLISH;
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.codec.Charsets.UTF_8;
+import static org.apache.commons.io.IOUtils.write;
+import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns;
+import static org.sonarqube.ws.MediaTypes.HTML;
+
+/**
+ * This filter provide the HTML file that will be used to display every web pages.
+ * The same file should be provided for any URLs except WS and static resources.
+ */
+public class WebPagesFilter implements Filter {
+
+ private static final String CACHE_CONTROL_HEADER = "Cache-Control";
+ private static final String CACHE_CONTROL_VALUE = "no-cache, no-store, must-revalidate";
+
+ private static final String CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
+
+ private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
+ .builder()
+ .excludes(staticResourcePatterns())
+ // These exclusions won't be needed anymore when this filter will be last filter (no more rails filter)
+ .excludes("/api/*", "/batch/*")
+ .build();
+
+ private String indexDotHtml;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+ String path = httpServletRequest.getRequestURI().replaceFirst(httpServletRequest.getContextPath(), "");
+ if (!URL_PATTERN.matches(path)) {
+ chain.doFilter(request, response);
+ return;
+ }
+ httpServletResponse.setContentType(HTML);
+ httpServletResponse.setCharacterEncoding(UTF_8.name().toLowerCase(ENGLISH));
+ httpServletResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_VALUE);
+ write(indexDotHtml, httpServletResponse.getOutputStream());
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ String context = filterConfig.getServletContext().getContextPath();
+ String indexFile = readIndexFile(filterConfig.getServletContext());
+ this.indexDotHtml = indexFile.replaceAll(CONTEXT_PLACEHOLDER, context);
+ }
+
+ private static String readIndexFile(ServletContext servletContext) {
+ try {
+ return IOUtils.toString(requireNonNull(servletContext.getResource("/index.html")), StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to provide index file", e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // Nothing to do
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
new file mode 100644
index 00000000000..d20a95a0b68
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
@@ -0,0 +1,190 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.platform.web;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class WebPagesFilterTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private HttpServletRequest request = mock(HttpServletRequest.class);
+ private HttpServletResponse response = mock(HttpServletResponse.class);
+ private FilterChain chain = mock(FilterChain.class);
+ private ServletContext servletContext = mock(ServletContext.class);
+ private FilterConfig filterConfig = mock(FilterConfig.class);
+ private StringOutputStream outputStream = new StringOutputStream();
+
+ private WebPagesFilter underTest = new WebPagesFilter();
+
+ @Before
+ public void setUp() throws Exception {
+ when(filterConfig.getServletContext()).thenReturn(servletContext);
+ when(response.getOutputStream()).thenReturn(outputStream);
+ }
+
+ @Test
+ public void verify_paths() throws Exception {
+ mockIndexFile();
+ verifyPathIsHandled("/");
+ verifyPathIsHandled("/issues");
+ verifyPathIsHandled("/foo");
+ verifyPthIsIgnored("/api/issues/search");
+ verifyPthIsIgnored("/batch/index");
+ }
+
+ @Test
+ public void return_index_file_content() throws Exception {
+ mockIndexFile();
+ mockPath("/foo", "");
+ underTest.init(filterConfig);
+ underTest.doFilter(request, response, chain);
+
+ assertThat(outputStream.toString()).contains("<head>");
+ verify(response).setContentType("text/html");
+ verify(response).setCharacterEncoding("utf-8");
+ verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ verify(response).getOutputStream();
+ verifyNoMoreInteractions(response);
+ }
+
+ @Test
+ public void return_index_file_content_with_default_web_context() throws Exception {
+ mockIndexFile();
+ mockPath("/foo", "");
+ underTest.init(filterConfig);
+ underTest.doFilter(request, response, chain);
+
+ assertThat(outputStream.toString()).contains("href=\"/sonar.css\"");
+ assertThat(outputStream.toString()).contains("<script src=\"/sonar.js\"></script>");
+ assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
+ }
+
+ @Test
+ public void return_index_file_content_with_web_context() throws Exception {
+ mockIndexFile();
+ mockPath("/foo", "/web");
+ underTest.init(filterConfig);
+ underTest.doFilter(request, response, chain);
+
+ assertThat(outputStream.toString()).contains("href=\"/web/sonar.css\"");
+ assertThat(outputStream.toString()).contains("<script src=\"/web/sonar.js\"></script>");
+ assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
+ }
+
+ @Test
+ public void fail_when_index_is_not_found() throws Exception {
+ mockPath("/foo", "");
+ when(servletContext.getResource("/index.html")).thenReturn(null);
+
+ expectedException.expect(IllegalStateException.class);
+ underTest.init(filterConfig);
+ }
+
+ private void mockIndexFile() throws MalformedURLException {
+ when(servletContext.getResource("/index.html")).thenReturn(getClass().getResource("WebPagesFilterTest/index.html"));
+ }
+
+ private void mockPath(String path, String context) throws MalformedURLException {
+ when(request.getRequestURI()).thenReturn(path);
+ when(request.getContextPath()).thenReturn(context);
+ when(servletContext.getContextPath()).thenReturn(context);
+ }
+
+ private void verifyPathIsHandled(String path) throws Exception {
+ mockPath(path, "");
+ underTest.init(filterConfig);
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response).getOutputStream();
+ verify(response).setContentType(anyString());
+ reset(response);
+ when(response.getOutputStream()).thenReturn(outputStream);
+ }
+
+ private void verifyPthIsIgnored(String path) throws Exception {
+ mockPath(path, "");
+ underTest.init(filterConfig);
+
+ underTest.doFilter(request, response, chain);
+
+ verifyZeroInteractions(response);
+ reset(response);
+ when(response.getOutputStream()).thenReturn(outputStream);
+ }
+
+ class StringOutputStream extends ServletOutputStream {
+ private StringBuffer buf = new StringBuffer();
+
+ StringOutputStream() {
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener listener) {
+
+ }
+
+ public void write(byte[] b) throws IOException {
+ this.buf.append(new String(b));
+ }
+
+ public void write(byte[] b, int off, int len) throws IOException {
+ this.buf.append(new String(b, off, len));
+ }
+
+ public void write(int b) throws IOException {
+ byte[] bytes = new byte[] {(byte) b};
+ this.buf.append(new String(bytes));
+ }
+
+ public String toString() {
+ return this.buf.toString();
+ }
+ }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html b/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html
new file mode 100644
index 00000000000..379f83f53c7
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <link href="%WEB_CONTEXT%/favicon.ico" rel="shortcut icon" type="image/x-icon">
+ <link href="%WEB_CONTEXT%/sonar.css" rel="stylesheet">
+ <title>SonarQube</title>
+</head>
+<body>
+<div id="content"></div>
+<script>window.baseUrl = '%WEB_CONTEXT%';</script>
+<script src="%WEB_CONTEXT%/sonar.js"></script>
+</body>
+</html>
diff --git a/server/sonar-web/public/index.html b/server/sonar-web/public/index.html
index 0864aeb17d2..cf8fd6ce5a4 100644
--- a/server/sonar-web/public/index.html
+++ b/server/sonar-web/public/index.html
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
- <meta charset="UTF-8">
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="%WEB_CONTEXT%/favicon.ico" rel="shortcut icon" type="image/x-icon">
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="%WEB_CONTEXT%/<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
@@ -15,4 +16,4 @@
<script src="%WEB_CONTEXT%/<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/web.xml b/server/sonar-web/src/main/webapp/WEB-INF/web.xml
index 865206d2030..5786dff2edb 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/web.xml
+++ b/server/sonar-web/src/main/webapp/WEB-INF/web.xml
@@ -64,6 +64,10 @@
<filter-name>RequestUidFilter</filter-name>
<filter-class>org.sonar.server.platform.web.requestid.RequestIdFilter</filter-class>
</filter>
+ <filter>
+ <filter-name>WebPagesFilter</filter-name>
+ <filter-class>org.sonar.server.platform.web.WebPagesFilter</filter-class>
+ </filter>
<!-- order of execution is important -->
<filter-mapping>
@@ -91,6 +95,10 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
+ <filter-name>WebPagesFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
<filter-name>RackFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java b/sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java
index d7677c4ea2b..f91ff21a0f8 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java
@@ -21,10 +21,9 @@ package org.sonarqube.ws;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
-import org.apache.commons.io.FilenameUtils;
-
import java.util.Locale;
import java.util.Map;
+import org.apache.commons.io.FilenameUtils;
/**
* @since 5.3
@@ -37,6 +36,7 @@ public final class MediaTypes {
public static final String PROTOBUF = "application/x-protobuf";
public static final String ZIP = "application/zip";
public static final String JAVASCRIPT = "application/javascript";
+ public static final String HTML = "text/html";
public static final String DEFAULT = "application/octet-stream";
private static final Map<String, String> MAP = new ImmutableMap.Builder<String, String>()
@@ -65,7 +65,7 @@ public final class MediaTypes {
.put("csv", "text/csv")
.put("properties", "text/plain")
.put("rtf", "text/rtf")
- .put("html", "text/html")
+ .put("html", HTML)
.put("css", "text/css")
.put("tsv", "text/tab-separated-values")
.build();