@@ -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()); |
@@ -19,67 +19,53 @@ | |||
*/ | |||
package org.sonar.server.app; | |||
import javax.annotation.Nullable; | |||
import org.apache.catalina.connector.Connector; | |||
import org.apache.catalina.startup.Tomcat; | |||
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_HTTP_KEEP_ALIVE_TIMEOUT; | |||
import static org.sonar.process.ProcessProperties.Property.WEB_PORT; | |||
/** | |||
* Configuration of Tomcat connectors | |||
*/ | |||
class TomcatConnectors { | |||
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; | |||
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)); | |||
} | |||
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 https://tomcat.apache.org/tomcat-8.5-doc/config/http.html | |||
// 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); | |||
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 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("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))); | |||
@@ -92,9 +78,4 @@ class TomcatConnectors { | |||
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); | |||
} | |||
} | |||
} |
@@ -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())); | |||
} | |||
} |
@@ -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) { |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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<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; | |||
})); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |