From: Antoine Vigneau Date: Mon, 18 Sep 2023 15:09:02 +0000 (+0200) Subject: SONAR-20489 Log the web port before starting Tomcat X-Git-Tag: 10.3.0.82913~354 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=968011861b5e72d0cb2eb7c5378b224e9c37d5b6;p=sonarqube.git SONAR-20489 Log the web port before starting Tomcat --- diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java index 5173c52e16b..c87a0f2e42a 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java @@ -19,14 +19,14 @@ */ package org.sonar.server.app; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; import com.google.common.base.Throwables; import java.io.File; import java.util.concurrent.CountDownLatch; import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.startup.Tomcat; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.process.Props; @@ -35,13 +35,17 @@ import static org.sonar.process.ProcessProperties.Property.PATH_TEMP; class EmbeddedTomcat { + private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedTomcat.class); + private final Props props; + private final TomcatHttpConnectorFactory tomcatHttpConnectorFactory; private Tomcat tomcat = null; private volatile StandardContext webappContext; private final CountDownLatch stopLatch = new CountDownLatch(1); - EmbeddedTomcat(Props props) { + EmbeddedTomcat(Props props, TomcatHttpConnectorFactory tomcatHttpConnectorFactory) { this.props = props; + this.tomcatHttpConnectorFactory = tomcatHttpConnectorFactory; } void start() { @@ -62,21 +66,30 @@ class EmbeddedTomcat { tomcat.getHost().setDeployOnStartup(true); new TomcatErrorHandling().configure(tomcat); new TomcatAccessLog().configure(tomcat, props); - TomcatConnectors.configure(tomcat, props); + tomcat.getService().addConnector(tomcatHttpConnectorFactory.createConnector(props)); webappContext = new TomcatContexts().configure(tomcat, props); try { - // let Tomcat temporarily log errors at start up - for example, port in use - Logger logger = (Logger) LoggerFactory.getLogger("org.apache.catalina.core.StandardService"); - logger.setLevel(Level.ERROR); tomcat.start(); - logger.setLevel(Level.OFF); - new TomcatStartupLogs(LoggerFactory.getLogger(getClass())).log(tomcat); + validateConnectorScheme(); } catch (LifecycleException e) { - LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to start web server", e); + LOGGER.error("Failed to start web server", e); Throwables.propagate(e); } } + private File tomcatBasedir() { + return new File(props.value(PATH_TEMP.getKey()), "tc"); + } + + private void validateConnectorScheme() { + Connector[] connectors = tomcat.getService().findConnectors(); + for (Connector connector : connectors) { + if (!connector.getScheme().equals("http")) { + throw new IllegalArgumentException("Unsupported connector: " + connector); + } + } + } + Status getStatus() { if (webappContext == null) { return Status.DOWN; @@ -94,10 +107,6 @@ class EmbeddedTomcat { DOWN, UP, FAILED } - private File tomcatBasedir() { - return new File(props.value(PATH_TEMP.getKey()), "tc"); - } - void terminate() { try { if (tomcat.getServer().getState().isAvailable()) { @@ -105,7 +114,7 @@ class EmbeddedTomcat { tomcat.stop(); tomcat.destroy(); } catch (Exception e) { - LoggerFactory.getLogger(EmbeddedTomcat.class).warn("Failed to stop web server", e); + LOGGER.warn("Failed to stop web server", e); } } deleteQuietly(tomcatBasedir()); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatConnectors.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatConnectors.java deleted file mode 100644 index 7f5487903d3..00000000000 --- a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatConnectors.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info 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.app; - -import javax.annotation.Nullable; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.sonar.process.Props; - -import static java.lang.String.format; -import static org.sonar.process.ProcessProperties.Property.WEB_HOST; -import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_ACCEPT_COUNT; -import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_MAX_THREADS; -import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_MIN_THREADS; -import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_KEEP_ALIVE_TIMEOUT; - -/** - * Configuration of Tomcat connectors - */ -class TomcatConnectors { - - static final String HTTP_PROTOCOL = "HTTP/1.1"; - static final int MAX_HTTP_HEADER_SIZE_BYTES = 48 * 1024; - private static final int MAX_POST_SIZE = -1; - - private TomcatConnectors() { - // only static stuff - } - - static void configure(Tomcat tomcat, Props props) { - Connector httpConnector = newHttpConnector(props); - tomcat.getService().addConnector(httpConnector); - } - - private static Connector newHttpConnector(Props props) { - // Not named "sonar.web.http.port" to keep backward-compatibility - int port = props.valueAsInt("sonar.web.port", 9000); - if (port < 0) { - throw new IllegalStateException(format("HTTP port '%s' is invalid", port)); - } - - Connector connector = new Connector(HTTP_PROTOCOL); - connector.setURIEncoding("UTF-8"); - connector.setProperty("address", props.value(WEB_HOST.getKey(), "0.0.0.0")); - connector.setProperty("socket.soReuseAddress", "true"); - // see https://tomcat.apache.org/tomcat-8.5-doc/config/http.html - connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}"); - configurePool(props, connector); - configureCompression(connector); - configureMaxHttpHeaderSize(connector); - connector.setPort(port); - connector.setMaxPostSize(MAX_POST_SIZE); - return connector; - } - - /** - * HTTP header must be at least 48kb to accommodate the authentication token used for - * negotiate protocol of windows authentication. - */ - private static void configureMaxHttpHeaderSize(Connector connector) { - setConnectorAttribute(connector, "maxHttpHeaderSize", MAX_HTTP_HEADER_SIZE_BYTES); - } - - private static void configurePool(Props props, Connector connector) { - connector.setProperty("acceptorThreadCount", String.valueOf(2)); - connector.setProperty("minSpareThreads", String.valueOf(props.valueAsInt(WEB_HTTP_MIN_THREADS.getKey(), 5))); - connector.setProperty("maxThreads", String.valueOf(props.valueAsInt(WEB_HTTP_MAX_THREADS.getKey(), 50))); - connector.setProperty("acceptCount", String.valueOf(props.valueAsInt(WEB_HTTP_ACCEPT_COUNT.getKey(), 25))); - connector.setProperty("keepAliveTimeout", String.valueOf(props.valueAsInt(WEB_HTTP_KEEP_ALIVE_TIMEOUT.getKey(), 60000))); - } - - private static void configureCompression(Connector connector) { - connector.setProperty("compression", "on"); - connector.setProperty("compressionMinSize", "1024"); - connector.setProperty("compressibleMimeType", "text/html,text/xml,text/plain,text/css,application/json,application/javascript,text/javascript"); - } - - private static void setConnectorAttribute(Connector c, String key, @Nullable Object value) { - if (value != null) { - c.setAttribute(key, value); - } - } -} diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatHttpConnectorFactory.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatHttpConnectorFactory.java new file mode 100644 index 00000000000..109edd7dcbd --- /dev/null +++ b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatHttpConnectorFactory.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info 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.app; + +import org.apache.catalina.connector.Connector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.process.Props; + +import static java.lang.String.format; +import static org.sonar.process.ProcessProperties.Property.WEB_HOST; +import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_ACCEPT_COUNT; +import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_KEEP_ALIVE_TIMEOUT; +import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_MAX_THREADS; +import static org.sonar.process.ProcessProperties.Property.WEB_HTTP_MIN_THREADS; +import static org.sonar.process.ProcessProperties.Property.WEB_PORT; + +public class TomcatHttpConnectorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(TomcatHttpConnectorFactory.class); + + static final String HTTP_PROTOCOL = "HTTP/1.1"; + // Max HTTP headers size must be 48kb to accommodate the authentication token used for negotiate protocol of windows authentication. + static final int MAX_HTTP_HEADER_SIZE_BYTES = 48 * 1024; + private static final int MAX_POST_SIZE = -1; + + public Connector createConnector(Props props) { + Connector connector = new Connector(HTTP_PROTOCOL); + connector.setURIEncoding("UTF-8"); + connector.setProperty("address", props.value(WEB_HOST.getKey(), "0.0.0.0")); + connector.setProperty("socket.soReuseAddress", "true"); + // See Tomcat configuration reference: https://tomcat.apache.org/tomcat-9.0-doc/config/http.html + connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}"); + connector.setProperty("maxHttpHeaderSize", String.valueOf(MAX_HTTP_HEADER_SIZE_BYTES)); + connector.setMaxPostSize(MAX_POST_SIZE); + configurePort(connector, props); + configurePool(props, connector); + configureCompression(connector); + return connector; + } + + private static void configurePort(Connector connector, Props props) { + int port = props.valueAsInt(WEB_PORT.getKey(), 9000); + if (port < 0) { + throw new IllegalStateException(format("HTTP port %s is invalid", port)); + } + connector.setPort(port); + LOGGER.info("Starting Tomcat on port {}", connector.getPort()); + } + + private static void configurePool(Props props, Connector connector) { + connector.setProperty("minSpareThreads", String.valueOf(props.valueAsInt(WEB_HTTP_MIN_THREADS.getKey(), 5))); + connector.setProperty("maxThreads", String.valueOf(props.valueAsInt(WEB_HTTP_MAX_THREADS.getKey(), 50))); + connector.setProperty("acceptCount", String.valueOf(props.valueAsInt(WEB_HTTP_ACCEPT_COUNT.getKey(), 25))); + connector.setProperty("keepAliveTimeout", String.valueOf(props.valueAsInt(WEB_HTTP_KEEP_ALIVE_TIMEOUT.getKey(), 60000))); + } + + private static void configureCompression(Connector connector) { + connector.setProperty("compression", "on"); + connector.setProperty("compressionMinSize", "1024"); + connector.setProperty("compressibleMimeType", "text/html,text/xml,text/plain,text/css,application/json,application/javascript,text/javascript"); + } + +} diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatStartupLogs.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatStartupLogs.java deleted file mode 100644 index 0fc545e88e0..00000000000 --- a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatStartupLogs.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info 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.app; - -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; - -class TomcatStartupLogs { - - private final Logger log; - - TomcatStartupLogs(Logger log) { - this.log = log; - } - - void log(Tomcat tomcat) { - Connector[] connectors = tomcat.getService().findConnectors(); - for (Connector connector : connectors) { - if (StringUtils.equalsIgnoreCase(connector.getScheme(), "http")) { - logHttp(connector); - } else { - throw new IllegalArgumentException("Unsupported connector: " + connector); - } - } - } - - private void logHttp(Connector connector) { - log.info(String.format("HTTP connector enabled on port %d", connector.getPort())); - } - -} diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java index 6c4ac6bbac2..528c63cb7bb 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java @@ -43,7 +43,7 @@ public class WebServer implements Monitored { .checkWritableTempDir() .checkRequiredJavaOptions(ImmutableMap.of("file.encoding", "UTF-8")); this.sharedDir = getSharedDir(props); - this.tomcat = new EmbeddedTomcat(props); + this.tomcat = new EmbeddedTomcat(props, new TomcatHttpConnectorFactory()); } private static File getSharedDir(Props props) { diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java index 5c4953f2d62..95a3ae67a9f 100644 --- a/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java +++ b/server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java @@ -20,10 +20,12 @@ package org.sonar.server.app; import java.io.File; +import java.io.IOException; import java.net.ConnectException; import java.net.InetAddress; import java.net.URL; import java.util.Properties; +import org.apache.catalina.connector.Connector; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; @@ -32,7 +34,10 @@ import org.sonar.process.NetworkUtilsImpl; import org.sonar.process.Props; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class EmbeddedTomcatTest { @@ -40,10 +45,65 @@ public class EmbeddedTomcatTest { public TemporaryFolder temp = new TemporaryFolder(); @Test - public void start() throws Exception { + public void start_shouldStartTomcatAndAcceptConnections() throws Exception { + InetAddress address = InetAddress.getLoopbackAddress(); + int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort(); + Props props = getProps(address, httpPort); + + EmbeddedTomcat tomcat = new EmbeddedTomcat(props, new TomcatHttpConnectorFactory()); + assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.DOWN); + tomcat.start(); + assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.UP); + + URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort); + assertThatCode(() -> url.openConnection().connect()) + .doesNotThrowAnyException(); + } + + @Test + public void start_whenWrongScheme_shouldThrow() throws IOException { + InetAddress address = InetAddress.getLoopbackAddress(); + int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort(); + Props props = getProps(address, httpPort); + + TomcatHttpConnectorFactory tomcatHttpConnectorFactory = mock(); + when(tomcatHttpConnectorFactory.createConnector(props)).thenReturn(getAJPConnector(props)); + EmbeddedTomcat tomcat = new EmbeddedTomcat(props, tomcatHttpConnectorFactory); + + assertThatThrownBy(tomcat::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format("Unsupported connector: Connector[AJP/1.3-%s]", httpPort)); + } + + private Connector getAJPConnector(Props props) { + Connector connector = new Connector("AJP/1.3"); + connector.setScheme("ajp"); + connector.setPort(props.valueAsInt("sonar.web.port")); + connector.setProperty("secretRequired", "false"); + return connector; + } + + @Test + public void terminate_shouldTerminateTomcatAndStopAcceptingConnections() throws IOException { + InetAddress address = InetAddress.getLoopbackAddress(); + int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort(); + Props props = getProps(address, httpPort); + + EmbeddedTomcat tomcat = new EmbeddedTomcat(props, new TomcatHttpConnectorFactory()); + tomcat.start(); + URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort); + + tomcat.terminate(); + + assertThatThrownBy(() -> url.openConnection().connect()) + .isInstanceOf(ConnectException.class) + .hasMessage("Connection refused"); + + } + + private Props getProps(InetAddress address, int httpPort) throws IOException { Props props = new Props(new Properties()); - // prepare file system File home = temp.newFolder(); File data = temp.newFolder(); File webDir = new File(home, "web"); @@ -54,27 +114,9 @@ public class EmbeddedTomcatTest { props.set("sonar.path.logs", temp.newFolder().getAbsolutePath()); // start server on a random port - InetAddress address = InetAddress.getLoopbackAddress(); - int httpPort = NetworkUtilsImpl.INSTANCE.getNextLoopbackAvailablePort(); props.set("sonar.web.host", address.getHostAddress()); props.set("sonar.web.port", String.valueOf(httpPort)); - EmbeddedTomcat tomcat = new EmbeddedTomcat(props); - assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.DOWN); - tomcat.start(); - assertThat(tomcat.getStatus()).isEqualTo(EmbeddedTomcat.Status.UP); - - // check that http connector accepts requests - URL url = new URL("http://" + address.getHostAddress() + ":" + httpPort); - url.openConnection().connect(); - - // stop server - tomcat.terminate(); - // tomcat.isUp() must not be called. It is used to wait for server startup, not shutdown. - try { - url.openConnection().connect(); - fail(); - } catch (ConnectException e) { - // expected - } + return props; } + } diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/app/StartupLogsTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/app/StartupLogsTest.java deleted file mode 100644 index 412dc7640af..00000000000 --- a/server/sonar-webserver/src/test/java/org/sonar/server/app/StartupLogsTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info 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.app; - -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.junit.Test; -import org.mockito.Mockito; -import org.slf4j.Logger; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -public class StartupLogsTest { - - private Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS); - private Logger logger = mock(Logger.class); - private TomcatStartupLogs underTest = new TomcatStartupLogs(logger); - - @Test - public void fail_with_IAE_on_unsupported_protocol() { - Connector connector = newConnector("AJP/1.3", "ajp"); - when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); - - assertThatThrownBy(() -> underTest.log(tomcat)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unsupported connector: Connector[AJP/1.3-1234]"); - } - - @Test - public void logHttp() { - Connector connector = newConnector("HTTP/1.1", "http"); - when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); - - underTest.log(tomcat); - - verify(logger).info("HTTP connector enabled on port 1234"); - verifyNoMoreInteractions(logger); - } - - @Test - public void unsupported_connector() { - 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 { - underTest.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/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java deleted file mode 100644 index b5b481d1fdf..00000000000 --- a/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info 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.app; - -import com.google.common.collect.ImmutableMap; -import java.net.InetAddress; -import java.util.Map; -import java.util.Properties; -import org.apache.catalina.startup.Tomcat; -import org.junit.Test; -import org.mockito.Mockito; -import org.sonar.process.Props; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class TomcatConnectorsTest { - - private static final int DEFAULT_PORT = 9000; - private Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS); - - @Test - public void configure_thread_pool() { - Properties p = new Properties(); - p.setProperty("sonar.web.http.minThreads", "2"); - p.setProperty("sonar.web.http.maxThreads", "30"); - p.setProperty("sonar.web.http.acceptCount", "20"); - Props props = new Props(p); - - TomcatConnectors.configure(tomcat, props); - - verifyHttpConnector(DEFAULT_PORT, ImmutableMap.of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20)); - } - - @Test - public void configure_defaults() { - Props props = new Props(new Properties()); - - TomcatConnectors.configure(tomcat, props); - - verifyHttpConnector(DEFAULT_PORT, ImmutableMap.of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25)); - } - - @Test - public void different_thread_pools_for_connectors() { - Properties p = new Properties(); - p.setProperty("sonar.web.http.minThreads", "2"); - Props props = new Props(p); - - TomcatConnectors.configure(tomcat, props); - - verifyHttpConnector(DEFAULT_PORT, ImmutableMap.of("minSpareThreads", 2)); - } - - @Test - public void fail_with_ISE_if_http_port_is_invalid() { - Properties p = new Properties(); - p.setProperty("sonar.web.port", "-1"); - - try { - TomcatConnectors.configure(tomcat, new Props(p)); - fail(); - } catch (IllegalStateException e) { - assertThat(e).hasMessage("HTTP port '-1' is invalid"); - } - } - - @Test - public void bind_to_all_addresses_by_default() { - Properties p = new Properties(); - p.setProperty("sonar.web.port", "9000"); - - TomcatConnectors.configure(tomcat, new Props(p)); - - verify(tomcat.getService()).addConnector(argThat(c -> c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("0.0.0.0"))); - } - - @Test - public void bind_to_specific_address() { - Properties p = new Properties(); - p.setProperty("sonar.web.port", "9000"); - p.setProperty("sonar.web.host", "1.2.3.4"); - - TomcatConnectors.configure(tomcat, new Props(p)); - - verify(tomcat.getService()) - .addConnector(argThat(c -> c.getScheme().equals("http") && c.getPort() == 9000 && ((InetAddress) c.getProperty("address")).getHostAddress().equals("1.2.3.4"))); - } - - @Test - public void test_max_http_header_size_for_http_connection() { - TomcatConnectors.configure(tomcat, new Props(new Properties())); - - verifyHttpConnector(DEFAULT_PORT, ImmutableMap.of("maxHttpHeaderSize", TomcatConnectors.MAX_HTTP_HEADER_SIZE_BYTES)); - } - - @Test - public void test_max_post_size_for_http_connection() { - Properties properties = new Properties(); - - Props props = new Props(properties); - TomcatConnectors.configure(tomcat, props); - verify(tomcat.getService()).addConnector(argThat(c -> c.getMaxPostSize() == -1)); - } - - private void verifyHttpConnector(int expectedPort, Map expectedProps) { - verify(tomcat.getService()).addConnector(argThat(c -> { - if (!c.getScheme().equals("http")) { - return false; - } - if (!c.getProtocol().equals(TomcatConnectors.HTTP_PROTOCOL)) { - return false; - } - if (c.getPort() != expectedPort) { - return false; - } - for (Map.Entry expectedProp : expectedProps.entrySet()) { - if (!expectedProp.getValue().equals(c.getProperty(expectedProp.getKey()))) { - return false; - } - } - return true; - })); - } -} diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatHttpConnectorFactoryTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatHttpConnectorFactoryTest.java new file mode 100644 index 00000000000..1ce5e2cd85b --- /dev/null +++ b/server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatHttpConnectorFactoryTest.java @@ -0,0 +1,121 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info 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.app; + +import java.net.Inet4Address; +import java.util.Properties; +import org.apache.catalina.connector.Connector; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTester; +import org.sonar.process.Props; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class TomcatHttpConnectorFactoryTest { + @Rule + public LogTester logTester = new LogTester(); + + private static TomcatHttpConnectorFactory tomcatHttpConnectorFactory = new TomcatHttpConnectorFactory(); + + @Test + public void createConnector_shouldUseHardcodedPropertiesWhereNeeded() { + Props props = getEmptyProps(); + Connector connector = tomcatHttpConnectorFactory.createConnector(props); + + // General properties + assertThat(connector.getURIEncoding()).isEqualTo("UTF-8"); + assertThat(connector.getProperty("socket.soReuseAddress")).isEqualTo("true"); + assertThat(connector.getProperty("relaxedQueryChars")).isEqualTo("\"<>[\\]^`{|}"); + assertThat(connector.getProperty("maxHttpHeaderSize")).isEqualTo(49152); + assertThat(connector.getMaxPostSize()).isEqualTo(-1); + // Compression properties + assertThat(connector.getProperty("compression")).isEqualTo("on"); + assertThat(connector.getProperty("compressionMinSize")).isEqualTo(1024); + assertThat(connector.getProperty("compressibleMimeType")).isEqualTo("text/html,text/xml,text/plain,text/css,application/json,application/javascript,text/javascript"); + } + + @Test + public void createConnector_whenPropertiesNotSet_shouldUseDefault() { + Props props = getEmptyProps(); + Connector connector = tomcatHttpConnectorFactory.createConnector(props); + + // General properties + assertAddress(connector.getProperty("address"), "0.0.0.0"); + // Port + assertThat(connector.getPort()).isEqualTo(9000); + // Pool properties + assertThat(connector.getProperty("minSpareThreads")).isEqualTo(5); + assertThat(connector.getProperty("maxThreads")).isEqualTo(50); + assertThat(connector.getProperty("acceptCount")).isEqualTo(25); + assertThat(connector.getProperty("keepAliveTimeout")).isEqualTo(60000); + } + + @Test + public void createConnector_whenPropertiesSet_shouldUseThem() { + Props props = getMeaningfulProps(); + Connector connector = tomcatHttpConnectorFactory.createConnector(props); + + // General properties + assertAddress(connector.getProperty("address"), "12.12.12.12"); + // Port + assertThat(connector.getPort()).isEqualTo(1234); + Assertions.assertThat(logTester.logs(Level.INFO)).contains("Starting Tomcat on port 1234"); + // Pool properties + assertThat(connector.getProperty("minSpareThreads")).isEqualTo(12); + assertThat(connector.getProperty("maxThreads")).isEqualTo(42); + assertThat(connector.getProperty("acceptCount")).isEqualTo(12); + assertThat(connector.getProperty("keepAliveTimeout")).isEqualTo(1000); + } + + @Test + public void createConnector_whenNotValidPort_shouldThrow() { + Props props = getEmptyProps(); + props.set("sonar.web.port", "-1"); + assertThatThrownBy(() -> tomcatHttpConnectorFactory.createConnector(props)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("HTTP port -1 is invalid"); + } + + private void assertAddress(Object address, String ip) { + assertThat(address).isInstanceOf(Inet4Address.class); + Inet4Address inet4Address = (Inet4Address) address; + assertThat(inet4Address.getHostAddress()).isEqualTo(ip); + } + + private Props getEmptyProps() { + return new Props(new Properties()); + } + + private Props getMeaningfulProps() { + Properties properties = new Properties(); + properties.setProperty("sonar.web.host", "12.12.12.12"); + properties.setProperty("sonar.web.port", "1234"); + properties.setProperty("sonar.web.http.minThreads", "12"); + properties.setProperty("sonar.web.http.maxThreads", "42"); + properties.setProperty("sonar.web.http.acceptCount", "12"); + properties.setProperty("sonar.web.http.keepAliveTimeout", "1000"); + return new Props(properties); + } + +} \ No newline at end of file