*/
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;
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() {
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;
DOWN, UP, FAILED
}
- private File tomcatBasedir() {
- return new File(props.value(PATH_TEMP.getKey()), "tc");
- }
-
void terminate() {
try {
if (tomcat.getServer().getState().isAvailable()) {
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());
+++ /dev/null
-/*
- * 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);
- }
- }
-}
--- /dev/null
+/*
+ * 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");
+ }
+
+}
+++ /dev/null
-/*
- * 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()));
- }
-
-}
.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) {
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;
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 {
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");
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;
}
+
}
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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<String, Object> 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<String, Object> expectedProp : expectedProps.entrySet()) {
- if (!expectedProp.getValue().equals(c.getProperty(expectedProp.getKey()))) {
- return false;
- }
- }
- return true;
- }));
- }
-}
--- /dev/null
+/*
+ * 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