Browse Source

SONAR-20489 Log the web port before starting Tomcat

tags/10.3.0.82913
Antoine Vigneau 8 months ago
parent
commit
968011861b

+ 24
- 15
server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java View File

@@ -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());

server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatConnectors.java → server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatHttpConnectorFactory.java View File

@@ -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);
}
}
}

+ 0
- 50
server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatStartupLogs.java View File

@@ -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()));
}

}

+ 1
- 1
server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java View File

@@ -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) {

+ 65
- 23
server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java View File

@@ -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;
}

}

+ 0
- 82
server/sonar-webserver/src/test/java/org/sonar/server/app/StartupLogsTest.java View File

@@ -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;
}
}

+ 0
- 145
server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java View File

@@ -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;
}));
}
}

+ 121
- 0
server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatHttpConnectorFactoryTest.java View File

@@ -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);
}

}

Loading…
Cancel
Save