From b50745d4f87179c3b77f383efbe3e5b934915c1a Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Sat, 31 Jan 2015 23:04:52 +0100 Subject: [PATCH] SONAR-6140 Ability to restrict list of HTTPS ciphers with the new propert sonar.web.https.ciphers --- .../java/org/sonar/server/app/Connectors.java | 12 +- .../org/sonar/server/app/EmbeddedTomcat.java | 4 + .../org/sonar/server/app/StartupLogs.java | 74 +++++++++++++ .../java/org/sonar/server/app/Webapp.java | 3 + .../sonar/server/app/EmbeddedTomcatTest.java | 78 +++++++++++++ .../org/sonar/server/app/StartupLogsTest.java | 104 ++++++++++++++++++ .../src/main/assembly/conf/sonar.properties | 9 ++ 7 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java b/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java index a402f4a8aa8..ff6557bcfc1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/Connectors.java @@ -21,7 +21,6 @@ package org.sonar.server.app; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; -import org.slf4j.LoggerFactory; import org.sonar.process.Props; import javax.annotation.Nullable; @@ -33,6 +32,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +/** + * Configuration of Tomcat connectors + */ class Connectors { private static final int DISABLED_PORT = -1; @@ -77,7 +79,6 @@ class Connectors { if (port > DISABLED_PORT) { connector = newConnector(props, HTTP_PROTOCOL, "http"); connector.setPort(port); - info("HTTP connector is enabled on port " + port); } return connector; } @@ -89,7 +90,6 @@ class Connectors { if (port > DISABLED_PORT) { connector = newConnector(props, AJP_PROTOCOL, "http"); connector.setPort(port); - info("AJP connector is enabled on port " + port); } return connector; } @@ -115,12 +115,12 @@ class Connectors { setConnectorAttribute(connector, "truststoreType", props.value("sonar.web.https.truststoreType", "JKS")); setConnectorAttribute(connector, "truststoreProvider", props.value("sonar.web.https.truststoreProvider")); setConnectorAttribute(connector, "clientAuth", props.value("sonar.web.https.clientAuth", "false")); + setConnectorAttribute(connector, "ciphers", props.value("sonar.web.https.ciphers")); // SSLv3 must not be enable because of Poodle vulnerability // See https://jira.codehaus.org/browse/SONAR-5860 setConnectorAttribute(connector, "sslEnabledProtocols", "TLSv1,TLSv1.1,TLSv1.2"); setConnectorAttribute(connector, "sslProtocol", "TLS"); setConnectorAttribute(connector, "SSLEnabled", true); - info("HTTPS connector is enabled on port " + port); } return connector; } @@ -153,8 +153,4 @@ class Connectors { c.setAttribute(key, value); } } - - private static void info(String message) { - LoggerFactory.getLogger(Connectors.class).info(message); - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java index 049b2d208fa..7d33ad00f11 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java @@ -60,12 +60,16 @@ class EmbeddedTomcat { webappContext = Webapp.configure(tomcat, props); try { tomcat.start(); + new StartupLogs(LoggerFactory.getLogger(getClass())).log(tomcat); } catch (LifecycleException e) { Throwables.propagate(e); } } boolean isReady() { + if (webappContext == null) { + return false; + } switch (webappContext.getState()) { case NEW: case INITIALIZING: diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java b/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java new file mode 100644 index 00000000000..2e94f2c2d99 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/app/StartupLogs.java @@ -0,0 +1,74 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.app; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.commons.lang.StringUtils; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; +import org.slf4j.Logger; + +class StartupLogs { + + private final Logger log; + + StartupLogs(Logger log) { + this.log = log; + } + + void log(Tomcat tomcat) { + Connector[] connectors = tomcat.getService().findConnectors(); + for (Connector connector : connectors) { + if (StringUtils.containsIgnoreCase(connector.getProtocol(), "AJP")) { + logAjp(connector); + } else if (StringUtils.equalsIgnoreCase(connector.getScheme(), "https")) { + logHttps(connector); + } else if (StringUtils.equalsIgnoreCase(connector.getScheme(), "http")) { + logHttp(connector); + } else { + throw new IllegalArgumentException("Unsupported connector: " + connector); + } + } + } + + private void logAjp(Connector connector) { + log.info(String.format("%s connector enabled on port %d", connector.getProtocol(), connector.getPort())); + } + + private void logHttp(Connector connector) { + log.info(String.format("HTTP connector enabled on port %d", connector.getPort())); + } + + private void logHttps(Connector connector) { + StringBuilder additional = new StringBuilder(); + ProtocolHandler protocol = connector.getProtocolHandler(); + if (protocol instanceof AbstractHttp11JsseProtocol) { + additional.append("| ciphers="); + String ciphers = ((AbstractHttp11JsseProtocol) protocol).getCiphers(); + if (StringUtils.isBlank(ciphers)) { + additional.append("JVM defaults"); + } else { + additional.append(ciphers); + } + } + log.info(String.format("HTTPS connector enabled on port %d %s", connector.getPort(), additional)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java index f858128b4b0..e31e3a0e0c9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java @@ -30,6 +30,9 @@ import org.sonar.process.Props; import java.io.File; import java.util.Map; +/** + * Configures webapp into Tomcat + */ class Webapp { private static final String JRUBY_MAX_RUNTIMES = "jruby.max.runtimes"; diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java new file mode 100644 index 00000000000..3dd916816e6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java @@ -0,0 +1,78 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.app; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.NetworkUtils; +import org.sonar.process.Props; + +import java.io.File; +import java.net.ConnectException; +import java.net.Inet4Address; +import java.net.URL; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public class EmbeddedTomcatTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void start() throws Exception { + Props props = new Props(new Properties()); + + // prepare file system + File home = temp.newFolder(); + File webDir = new File(home, "web"); + FileUtils.write(new File(home, "web/WEB-INF/web.xml"), ""); + props.set("sonar.path.home", home.getAbsolutePath()); + props.set("sonar.path.web", webDir.getAbsolutePath()); + + // start server on a random port + int httpPort = NetworkUtils.freePort(); + int ajpPort = NetworkUtils.freePort(); + props.set("sonar.web.port", String.valueOf(httpPort)); + props.set("sonar.ajp.port", String.valueOf(ajpPort)); + EmbeddedTomcat tomcat = new EmbeddedTomcat(props); + assertThat(tomcat.isReady()).isFalse(); + tomcat.start(); + assertThat(tomcat.isReady()).isTrue(); + + // check that http connector accepts requests + URL url = new URL("http://" + Inet4Address.getLocalHost().getHostAddress() + ":" + httpPort); + url.openConnection().connect(); + + // stop server + tomcat.terminate(); + // tomcat.isReady() must not be called. It is used to wait for server startup, not shutdown. + try { + url.openConnection().connect(); + fail(); + } catch (ConnectException e) { + // expected + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java b/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java new file mode 100644 index 00000000000..03bbbc06085 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/app/StartupLogsTest.java @@ -0,0 +1,104 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.app; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.Http11Protocol; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.*; + +public class StartupLogsTest { + + Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS); + Logger logger = mock(Logger.class); + StartupLogs sut = new StartupLogs(logger); + + @Test + public void logAjp() throws Exception { + Connector connector = newConnector("AJP/1.3", "http"); + when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); + + sut.log(tomcat); + + verify(logger).info("AJP/1.3 connector enabled on port 1234"); + verifyNoMoreInteractions(logger); + } + + @Test + public void logHttp() throws Exception { + Connector connector = newConnector("HTTP/1.1", "http"); + when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); + + sut.log(tomcat); + + verify(logger).info("HTTP connector enabled on port 1234"); + verifyNoMoreInteractions(logger); + } + + @Test + public void logHttps_default_ciphers() throws Exception { + Connector connector = newConnector("HTTP/1.1", "https"); + when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); + + sut.log(tomcat); + + verify(logger).info("HTTPS connector enabled on port 1234 | ciphers=JVM defaults"); + verifyNoMoreInteractions(logger); + } + + @Test + public void logHttps_overridden_ciphers() throws Exception { + Connector connector = newConnector("HTTP/1.1", "https"); + connector.setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol"); + ((Http11Protocol) connector.getProtocolHandler()).setCiphers("SSL_RSA,TLS_RSA_WITH_RC4"); + when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); + + sut.log(tomcat); + + verify(logger).info("HTTPS connector enabled on port 1234 | ciphers=SSL_RSA,TLS_RSA_WITH_RC4"); + verifyNoMoreInteractions(logger); + } + + @Test + public void unsupported_connector() throws Exception { + Connector connector = mock(Connector.class, Mockito.RETURNS_DEEP_STUBS); + when(connector.getProtocol()).thenReturn("SPDY/1.1"); + when(connector.getScheme()).thenReturn("spdy"); + when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); + try { + sut.log(tomcat); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + private Connector newConnector(String protocol, String schema) { + Connector httpConnector = new Connector(protocol); + httpConnector.setScheme(schema); + httpConnector.setPort(1234); + return httpConnector; + } +} diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index 2b9c3dbdedd..84feed3c14e 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -154,6 +154,15 @@ # and 'true' (certificates are required). #sonar.web.https.clientAuth=false +# HTTPS - comma separated list of encryption ciphers to support for HTTPS connections. +# If specified, only the ciphers that are listed and supported by the SSL implementation will be used. +# By default, the default ciphers for the JVM will be used. Note that this usually means that the weak +# export grade ciphers will be included in the list of available ciphers. +# The ciphers are specified using the JSSE cipher naming convention (see +# https://www.openssl.org/docs/apps/ciphers.html) +# Example: sonar.web.https.ciphers=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 +#sonar.web.https.ciphers= + # The maximum number of connections that the server will accept and process at any given time. # When this number has been reached, the server will not accept any more connections until # the number of connections falls below this value. The operating system may still accept connections -- 2.39.5