]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20489 Log the web port before starting Tomcat
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>
Mon, 18 Sep 2023 15:09:02 +0000 (17:09 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 26 Sep 2023 10:37:13 +0000 (10:37 +0000)
server/sonar-webserver/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatConnectors.java [deleted file]
server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatHttpConnectorFactory.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatStartupLogs.java [deleted file]
server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java
server/sonar-webserver/src/test/java/org/sonar/server/app/EmbeddedTomcatTest.java
server/sonar-webserver/src/test/java/org/sonar/server/app/StartupLogsTest.java [deleted file]
server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatConnectorsTest.java [deleted file]
server/sonar-webserver/src/test/java/org/sonar/server/app/TomcatHttpConnectorFactoryTest.java [new file with mode: 0644]

index 5173c52e16b7184abff1343fea922e8d0b77d186..c87a0f2e42a14a8401c4574b89b42f82add4ea99 100644 (file)
  */
 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 (file)
index 7f54879..0000000
+++ /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 (file)
index 0000000..109edd7
--- /dev/null
@@ -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 (file)
index 0fc545e..0000000
+++ /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()));
-  }
-
-}
index 6c4ac6bbac2f8a153a9d41bf174abae78c189b08..528c63cb7bb2ec94696ee360c1a2a10e27c0dc0e 100644 (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) {
index 5c4953f2d622061b54085b680a01a8492d55e5b7..95a3ae67a9fe8a1e0ca4a35ad481ce5fa04fa5eb 100644 (file)
 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 (file)
index 412dc76..0000000
+++ /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 (file)
index b5b481d..0000000
+++ /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<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;
-    }));
-  }
-}
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 (file)
index 0000000..1ce5e2c
--- /dev/null
@@ -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