]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9135 Add safety checks in Cluster configuration
authorEric Hartmann <hartmann.eric@gmail.com>
Wed, 3 May 2017 13:11:31 +0000 (15:11 +0200)
committerEric Hartmann <hartmann.eric@gmail.Com>
Fri, 5 May 2017 12:41:40 +0000 (14:41 +0200)
server/sonar-process-monitor/src/main/java/org/sonar/application/config/ClusterSettings.java
server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsLoopbackTest.java [new file with mode: 0644]
server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsTest.java
server/sonar-process-monitor/src/test/java/org/sonar/application/config/TestAppSettings.java
server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaProcessLauncherImplTest.java

index dd2318d696423628561daf0447b252d8cea58414..a5dd6996fc9994429fc0f6c8e0cc517f706b11bc 100644 (file)
  */
 package org.sonar.application.config;
 
+import com.google.common.net.InetAddresses;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
+import org.apache.commons.lang.StringUtils;
 import org.sonar.process.MessageException;
 import org.sonar.process.ProcessId;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
 
+import static com.google.common.net.InetAddresses.forString;
 import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static org.apache.commons.lang.StringUtils.isBlank;
 import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 import static org.sonar.process.ProcessProperties.CLUSTER_WEB_LEADER;
 import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
 
 public class ClusterSettings implements Consumer<Props> {
 
@@ -40,13 +53,96 @@ public class ClusterSettings implements Consumer<Props> {
     if (!isClusterEnabled(props)) {
       return;
     }
+
+    checkProperties(props);
+  }
+
+  private static void checkProperties(Props props) {
+    // Cluster web leader is not allowed
     if (props.value(CLUSTER_WEB_LEADER) != null) {
       throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_LEADER));
     }
+
+    // Mandatory properties
+    ensureMandatoryProperty(props, SEARCH_HOST);
+    ensureMandatoryProperty(props, CLUSTER_HOSTS);
+    ensureMandatoryProperty(props, CLUSTER_SEARCH_HOSTS);
+
+    // H2 Database is not allowed in cluster mode
     String jdbcUrl = props.value(JDBC_URL);
-    if (jdbcUrl == null || jdbcUrl.startsWith("jdbc:h2:")) {
+    if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
       throw new MessageException("Embedded database is not supported in cluster mode");
     }
+
+    // Loopback interfaces are forbidden for SEARCH_HOST and CLUSTER_NETWORK_INTERFACES
+    ensureNotLoopback(props, SEARCH_HOST);
+    ensureNotLoopback(props, CLUSTER_HOSTS);
+    ensureNotLoopback(props, CLUSTER_NETWORK_INTERFACES);
+    ensureNotLoopback(props, CLUSTER_SEARCH_HOSTS);
+
+    ensureLocalAddress(props, SEARCH_HOST);
+    ensureLocalAddress(props, CLUSTER_NETWORK_INTERFACES);
+  }
+
+  private static void ensureMandatoryProperty(Props props, String key) {
+    if (isBlank(props.value(key))) {
+      throw new MessageException(format("Property [%s] is mandatory", key));
+    }
+  }
+
+  private static void ensureNotLoopback(Props props, String key) {
+    String ipList = props.value(key);
+    if (ipList == null) {
+      return;
+    }
+
+    stream(ipList.split(","))
+      .filter(StringUtils::isNotBlank)
+      .map(StringUtils::trim)
+      .forEach(ip -> {
+        InetAddress inetAddress = convertToInetAddress(ip, key);
+        if (inetAddress.isLoopbackAddress()) {
+          throw new MessageException(format("The interface address [%s] of [%s] must not be a loopback address", ip, key));
+        }
+      });
+  }
+
+  private static void ensureLocalAddress(Props props, String key) {
+    String ipList = props.value(key);
+
+    if (ipList == null) {
+      return;
+    }
+
+    stream(ipList.split(","))
+      .filter(StringUtils::isNotBlank)
+      .map(StringUtils::trim)
+      .forEach(ip -> {
+        InetAddress inetAddress = convertToInetAddress(ip, key);
+        try {
+          if (NetworkInterface.getByInetAddress(inetAddress) == null) {
+            throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+          }
+        } catch (SocketException e) {
+          throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+        }
+      });
+  }
+
+  private static InetAddress convertToInetAddress(String text, String key) {
+    InetAddress inetAddress;
+
+    if (!InetAddresses.isInetAddress(text)) {
+      try {
+        inetAddress =InetAddress.getByName(text);
+      } catch (UnknownHostException e) {
+        throw new MessageException(format("The interface address [%s] of [%s] cannot be resolved : %s", text, key, e.getMessage()));
+      }
+    } else {
+      inetAddress = forString(text);
+    }
+
+    return inetAddress;
   }
 
   public static boolean isClusterEnabled(AppSettings settings) {
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsLoopbackTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsLoopbackTest.java
new file mode 100644 (file)
index 0000000..594a921
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.application.config;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Enumeration;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.process.MessageException;
+
+import static java.lang.String.format;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
+@RunWith(Theories.class)
+public class ClusterSettingsLoopbackTest {
+
+  private TestAppSettings settings;
+
+  @DataPoints("loopback_with_single_ip")
+  public static final String[] LOOPBACK_SINGLE_IP = {
+    "localhost",
+    "ip6-localhost",
+    "127.0.0.1",
+    "127.1.1.1",
+    "127.243.136.241",
+    "::1",
+    "0:0:0:0:0:0:0:1"
+  };
+
+  @DataPoints("loopback_with_multiple_ips")
+  public static final String[] LOOPBACK_IPS = {
+    "localhost",
+    "ip6-localhost",
+    "127.0.0.1",
+    "127.1.1.1",
+    "127.243.136.241",
+    "::1",
+    "0:0:0:0:0:0:0:1",
+    "127.0.0.1,192.168.11.25",
+    "192.168.11.25,127.1.1.1",
+    "2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,0:0:0:0:0:0:0:1",
+    "0:0:0:0:0:0:0:1,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb",
+    "2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,::1",
+    "::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb",
+    "::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb"
+  };
+
+  @DataPoints("key_for_single_ip")
+  public static final String[] PROPERTY_KEYS_WITH_SINGLE_IP = {
+    SEARCH_HOST
+  };
+
+  @DataPoints("key_for_multiple_ips")
+  public static final String[] PROPERTY_KEYS_WITH_MULTIPLE_IPS = {
+    CLUSTER_NETWORK_INTERFACES,
+    CLUSTER_SEARCH_HOSTS,
+    CLUSTER_HOSTS
+  };
+
+  @DataPoints("key_with_local_ip")
+  public static final String[] PROPERTY_KEYS_ALL = {
+    CLUSTER_NETWORK_INTERFACES,
+    SEARCH_HOST
+  };
+
+  @DataPoints("not_local_address")
+  public static final String[] NOT_LOCAL_ADDRESS = {
+    "www.sonarqube.org",
+    "www.google.fr",
+    "www.google.com, www.sonarsource.com, wwww.sonarqube.org"
+  };
+
+  @DataPoints("unresolvable_hosts")
+  public static final String[] UNRESOLVABLE_HOSTS = {
+    "...",
+    "භඦආ\uD801\uDC8C\uD801\uDC8B"
+  };
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Before
+  public void resetSettings() {
+    settings = getClusterSettings();
+  }
+
+  @Theory
+  public void accept_throws_MessageException_if_not_local_address(
+    @FromDataPoints("key_with_local_ip") String propertyKey,
+    @FromDataPoints("not_local_address") String inet) {
+    settings.set(propertyKey, inet);
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage(" is not a local address");
+
+    new ClusterSettings().accept(settings.getProps());
+  }
+
+  @Theory
+  public void accept_throws_MessageException_if_unresolvable_host(
+    @FromDataPoints("key_with_local_ip") String propertyKey,
+    @FromDataPoints("unresolvable_hosts") String inet) {
+    settings.set(propertyKey, inet);
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage(" cannot be resolved");
+
+    new ClusterSettings().accept(settings.getProps());
+  }
+
+  @Theory
+  public void accept_throws_MessageException_if_loopback(
+    @FromDataPoints("key_for_single_ip") String propertyKey,
+    @FromDataPoints("loopback_with_single_ip") String inet) {
+    settings.set(propertyKey, inet);
+    checkLoopback(propertyKey);
+  }
+
+  @Theory
+  public void accept_throws_MessageException_if_loopback_for_multiple_ips(
+    @FromDataPoints("key_for_multiple_ips") String propertyKey,
+    @FromDataPoints("loopback_with_multiple_ips") String inet) {
+    settings.set(propertyKey, inet);
+    checkLoopback(propertyKey);
+  }
+
+  private void checkLoopback(String key) {
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage(format(" of [%s] must not be a loopback address",  key));
+
+    new ClusterSettings().accept(settings.getProps());
+  }
+
+  private static TestAppSettings getClusterSettings()  {
+    String localAddress = null;
+    try {
+      Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+      for (NetworkInterface networkInterface : Collections.list(nets)) {
+        if (!networkInterface.isLoopback() && networkInterface.isUp()) {
+          localAddress = networkInterface.getInetAddresses().nextElement().getHostAddress();
+        }
+      }
+      if (localAddress == null) {
+        throw new RuntimeException("Cannot find a non loopback card required for tests");
+      }
+
+    } catch (SocketException e) {
+      throw new RuntimeException("Cannot find a non loopback card required for tests");
+    }
+
+    TestAppSettings testAppSettings = new TestAppSettings()
+      .set(CLUSTER_ENABLED, "true")
+      .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+      .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+      .set(SEARCH_HOST, localAddress)
+      .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+    return testAppSettings;
+  }
+}
index 079cbf49b6aa5947a8f8d1ba3a85880195d812ec..3737f42ee220dc5ccef558449fac0c93c3f67af4 100644 (file)
  */
 package org.sonar.application.config;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.process.MessageException;
 import org.sonar.process.ProcessProperties;
 
+import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
 import static org.sonar.process.ProcessId.ELASTICSEARCH;
 import static org.sonar.process.ProcessId.WEB_SERVER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
 
 public class ClusterSettingsTest {
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private TestAppSettings settings = new TestAppSettings();
+  private TestAppSettings settings;
+
+  @Before
+  public void resetSettings() {
+    settings = getClusterSettings();
+  }
 
   @Test
   public void test_isClusterEnabled() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
     assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
 
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+    settings.set(CLUSTER_ENABLED, "false");
     assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
   }
 
   @Test
   public void isClusterEnabled_returns_false_by_default() {
-    assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
+    assertThat(ClusterSettings.isClusterEnabled(new TestAppSettings())).isFalse();
   }
 
   @Test
@@ -58,14 +71,14 @@ public class ClusterSettingsTest {
 
   @Test
   public void getEnabledProcesses_returns_all_processes_by_default_in_cluster_mode() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
 
     assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
   }
 
   @Test
   public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
     settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
 
     assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
@@ -73,7 +86,7 @@ public class ClusterSettingsTest {
 
   @Test
   public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
     settings.set("sonar.cluster.web.startupLeader", "true");
 
     expectedException.expect(MessageException.class);
@@ -84,7 +97,7 @@ public class ClusterSettingsTest {
 
   @Test
   public void accept_does_nothing_if_cluster_is_disabled() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+    settings.set(CLUSTER_ENABLED, "false");
     // this property is supposed to fail if cluster is enabled
     settings.set("sonar.cluster.web.startupLeader", "true");
 
@@ -93,7 +106,7 @@ public class ClusterSettingsTest {
 
   @Test
   public void accept_throws_MessageException_if_h2() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
     settings.set("sonar.jdbc.url", "jdbc:h2:mem");
 
     expectedException.expect(MessageException.class);
@@ -104,7 +117,7 @@ public class ClusterSettingsTest {
 
   @Test
   public void accept_throws_MessageException_if_default_jdbc_url() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.clearProperty(JDBC_URL);
 
     expectedException.expect(MessageException.class);
     expectedException.expectMessage("Embedded database is not supported in cluster mode");
@@ -119,16 +132,67 @@ public class ClusterSettingsTest {
 
   @Test
   public void isLocalElasticsearchEnabled_returns_true_by_default_in_cluster_mode() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
-
     assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
   }
 
   @Test
   public void isLocalElasticsearchEnabled_returns_false_if_local_es_node_is_disabled_in_cluster_mode() {
-    settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    settings.set(CLUSTER_ENABLED, "true");
     settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
 
     assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
   }
+
+  @Test
+  public void accept_throws_MessageException_if_searchHost_is_missing() {
+    settings.clearProperty(SEARCH_HOST);
+    checkMandatoryProperty(SEARCH_HOST);
+  }
+
+  @Test
+  public void accept_throws_MessageException_if_searchHost_is_blank() {
+    settings.set(SEARCH_HOST, " ");
+    checkMandatoryProperty(SEARCH_HOST);
+  }
+
+  @Test
+  public void accept_throws_MessageException_if_clusterHosts_is_missing() {
+    settings.clearProperty(CLUSTER_HOSTS);
+    checkMandatoryProperty(CLUSTER_HOSTS);
+  }
+
+  @Test
+  public void accept_throws_MessageException_if_clusterHosts_is_blank() {
+    settings.set(CLUSTER_HOSTS, " ");
+    checkMandatoryProperty(CLUSTER_HOSTS);
+  }
+
+  @Test
+  public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() {
+    settings.clearProperty(CLUSTER_SEARCH_HOSTS);
+    checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+  }
+
+  @Test
+  public void accept_throws_MessageException_if_clusterSearchHosts_is_blank() {
+    settings.set(CLUSTER_SEARCH_HOSTS, " ");
+    checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+  }
+
+  private void checkMandatoryProperty(String key) {
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage(format("Property [%s] is mandatory", key));
+
+    new ClusterSettings().accept(settings.getProps());
+  }
+
+  private static TestAppSettings getClusterSettings() {
+    TestAppSettings testAppSettings = new TestAppSettings()
+      .set(CLUSTER_ENABLED, "true")
+      .set(CLUSTER_SEARCH_HOSTS, "localhost")
+      .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+      .set(SEARCH_HOST, "192.168.233.1")
+      .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+    return testAppSettings;
+  }
 }
index 58971dd04b97948e65f82f55fa4fe5fa0124bacd..18f063f340bde59811da69c836995827b12a3e6a 100644 (file)
@@ -31,30 +31,34 @@ import org.sonar.process.Props;
  */
 public class TestAppSettings implements AppSettings {
 
-  private Props properties;
+  private Props props;
 
   public TestAppSettings() {
-    this.properties = new Props(new Properties());
-    ProcessProperties.completeDefaults(this.properties);
+    this.props = new Props(new Properties());
+    ProcessProperties.completeDefaults(this.props);
   }
 
   public TestAppSettings set(String key, String value) {
-    this.properties.set(key, value);
+    this.props.set(key, value);
     return this;
   }
 
   @Override
   public Props getProps() {
-    return properties;
+    return props;
   }
 
   @Override
   public Optional<String> getValue(String key) {
-    return Optional.ofNullable(properties.value(key));
+    return Optional.ofNullable(props.value(key));
   }
 
   @Override
   public void reload(Props copy) {
-    this.properties = copy;
+    this.props = copy;
+  }
+
+  public void clearProperty(String key) {
+    this.props.rawProperties().remove(key);
   }
 }
index 3beb208ec25c808bb653f86d6f656355bfa99556..988d1b56e193c113828038ed256523706963a591 100644 (file)
@@ -30,10 +30,6 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
-import org.sonar.application.process.JavaCommand;
-import org.sonar.application.process.JavaProcessLauncher;
-import org.sonar.application.process.JavaProcessLauncherImpl;
-import org.sonar.application.process.ProcessMonitor;
 import org.sonar.process.AllProcessesCommands;
 import org.sonar.process.ProcessId;