]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14682 - Filtering local network interfaces on webhooks
authorBelen Pruvost <belen.pruvost@sonarsource.com>
Wed, 14 Apr 2021 13:31:51 +0000 (15:31 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 14 Apr 2021 20:03:29 +0000 (20:03 +0000)
24 files changed:
server/sonar-db-migration/build.gradle
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DbVersion89.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProviderTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java
server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java
server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java
server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/CreateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/DeleteActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/ListActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/UpdateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java
server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker [new file with mode: 0644]

index ebdb837087e4532e53273eec1198d1d2101affc2..bdf5e2b38d1248c85d945ded90224798358b6f2b 100644 (file)
@@ -21,6 +21,7 @@ dependencies {
   testCompile 'com.google.code.findbugs:jsr305'
   testCompile 'com.tngtech.java:junit-dataprovider'
   testCompile 'commons-dbutils:commons-dbutils'
+  testCompile 'com.squareup.okhttp3:mockwebserver'
   testCompile 'junit:junit'
   testCompile 'org.assertj:assertj-core'
   testCompile 'org.mindrot:jbcrypt'
index 1b393d2d3c14dabb1a55379f178edc04636001f0..29853fbbb5ebe7e344e738cf11addc936b6851f8 100644 (file)
@@ -37,6 +37,7 @@ import org.sonar.server.platform.db.migration.version.v86.DbVersion86;
 import org.sonar.server.platform.db.migration.version.v87.DbVersion87;
 import org.sonar.server.platform.db.migration.version.v88.DbVersion88;
 import org.sonar.server.platform.db.migration.version.v89.DbVersion89;
+import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider;
 
 public class MigrationConfigurationModule extends Module {
   @Override
@@ -65,6 +66,7 @@ public class MigrationConfigurationModule extends Module {
 
       // Utility classes
       DbPrimaryKeyConstraintFinder.class,
-      DropPrimaryKeySqlGenerator.class);
+      DropPrimaryKeySqlGenerator.class,
+      NetworkInterfaceProvider.class);
   }
 }
index b160344075e78bb18dd5f3191fe2123535a60d81..0c9ff6e637a17e5bf27344da5ff93ee766afe387 100644 (file)
@@ -27,6 +27,7 @@ public class DbVersion89 implements DbVersion {
   @Override
   public void addSteps(MigrationStepRegistry registry) {
     registry
-      .add(4400, "Add indices on columns 'type' and 'value' to 'new_code_periods' table", AddIndicesToNewCodePeriodTable.class);
+      .add(4400, "Add indices on columns 'type' and 'value' to 'new_code_periods' table", AddIndicesToNewCodePeriodTable.class)
+      .add(4401, "Drop local webhooks", DropLocalWebhooks.class);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java
new file mode 100644 (file)
index 0000000..41b77b1
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.platform.db.migration.version.v89;
+
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.sql.SQLException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider;
+
+public class DropLocalWebhooks extends DataChange {
+  private static final Logger LOG = Loggers.get(DropLocalWebhooks.class);
+
+  private final NetworkInterfaceProvider networkInterfaceProvider;
+
+  public DropLocalWebhooks(Database db, NetworkInterfaceProvider networkInterfaceProvider) {
+    super(db);
+    this.networkInterfaceProvider = networkInterfaceProvider;
+  }
+
+  @Override
+  protected void execute(Context context) throws SQLException {
+    MassUpdate massUpdate = context.prepareMassUpdate();
+    massUpdate.select("select w.uuid, w.name, w.url, w.project_uuid, p.name from webhooks w left join projects p on p.uuid = w.project_uuid");
+    massUpdate.update("delete from webhooks where uuid = ?");
+    massUpdate.execute((row, update) -> {
+      try {
+        String webhookName = row.getString(2);
+        String webhookUrl = row.getString(3);
+        URL url = new URL(webhookUrl);
+        InetAddress address = InetAddress.getByName(url.getHost());
+        if (isLocalAddress(address)) {
+          boolean projectLevel = row.getString(4) != null;
+          if (projectLevel) {
+            String projectName = row.getString(5);
+            LOG.warn("Webhook '{}' for project '{}' has been removed because it used an invalid, unsafe URL. Please recreate " +
+              "this webhook with a valid URL or ask a project administrator to do it if it is still needed.", webhookName, projectName);
+          } else {
+            LOG.warn("Global webhook '{}' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL" +
+              " if it is still needed.", webhookName);
+          }
+
+          update.setString(1, row.getString(1));
+          return true;
+        }
+      } catch (MalformedURLException | UnknownHostException | SocketException e) {
+        return false;
+      }
+
+      return false;
+    });
+  }
+
+  private boolean isLocalAddress(InetAddress address) throws SocketException {
+    return networkInterfaceProvider.getNetworkInterfaceAddresses().stream()
+      .anyMatch(a -> a != null && a.equals(address));
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.java
new file mode 100644 (file)
index 0000000..e3df6bd
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.platform.db.migration.version.v89.util;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class NetworkInterfaceProvider {
+
+  public List<InetAddress> getNetworkInterfaceAddresses() throws SocketException {
+    return Collections.list(NetworkInterface.getNetworkInterfaces())
+      .stream()
+      .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream())
+      .collect(Collectors.toList());
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java
new file mode 100644 (file)
index 0000000..765f769
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.platform.db.migration.version.v89;
+
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.sql.SQLException;
+import java.util.List;
+import javax.annotation.Nullable;
+import okhttp3.HttpUrl;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DropLocalWebhooksTest {
+
+  @Rule
+  public LogTester logTester = new LogTester();
+
+  private static final String TABLE_NAME = "webhooks";
+
+  @Rule
+  public CoreDbTester dbTester = CoreDbTester.createForSchema(DropLocalWebhooksTest.class, "schema.sql");
+
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+
+  private final DataChange underTest = new DropLocalWebhooks(dbTester.database(), networkInterfaceProvider);
+
+  @Before
+  public void prepare() throws IOException {
+    InetAddress inetAddress1 = InetAddress.getByName(HttpUrl.parse("https://0.0.0.0/some_webhook").host());
+    InetAddress inetAddress2 = InetAddress.getByName(HttpUrl.parse("https://127.0.0.1/some_webhook").host());
+    InetAddress inetAddress3 = InetAddress.getByName(HttpUrl.parse("https://localhost/some_webhook").host());
+
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses())
+      .thenReturn(ImmutableList.of(inetAddress1, inetAddress2, inetAddress3));
+  }
+
+  @Test
+  public void execute() throws SQLException {
+    prepareWebhooks();
+
+    underTest.execute();
+
+    verifyMigrationResult();
+  }
+
+  @Test
+  public void migrationIsReEntrant() throws SQLException {
+    prepareWebhooks();
+
+    underTest.execute();
+    underTest.execute();
+
+    verifyMigrationResult();
+  }
+
+  @Test
+  public void migrationIsSuccessfulWhenNoWebhooksDeleted() throws SQLException {
+    insertProject("p1", "pn1");
+    insertWebhook("uuid-1", "https://10.15.15.15:5555/some_webhook", "p1");
+    insertWebhook("uuid-5", "https://some.valid.address.com/random_webhook", null);
+
+    underTest.execute();
+
+    assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isEqualTo(2);
+    assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty();
+  }
+
+  @Test
+  public void migrationIsSuccessfulWhenNoWebhooksInDb() throws SQLException {
+    insertProject("p1", "pn1");
+
+    underTest.execute();
+
+    assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isZero();
+    assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty();
+  }
+
+  private void prepareWebhooks() {
+    insertProject("p1", "pn1");
+    insertProject("p2", "pn2");
+    insertWebhook("uuid-1", "https://10.15.15.15:5555/some_webhook", "p1");
+    insertWebhook("uuid-2", "https://0.0.0.0/some_webhook", "p1");
+    insertWebhook("uuid-3", "https://172.16.16.16:6666/some_webhook", "p2");
+    insertWebhook("uuid-4", "https://127.0.0.1/some_webhook", "p2");
+    insertWebhook("uuid-5", "https://some.valid.address.com/random_webhook", null);
+    insertWebhook("uuid-6", "https://248.235.76.254:7777/some_webhook", null);
+    insertWebhook("uuid-7", "https://localhost/some_webhook", null);
+  }
+
+  private void verifyMigrationResult() {
+    assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isEqualTo(4);
+    assertThat(dbTester.select("select uuid from " + TABLE_NAME).stream().map(columns -> columns.get("UUID")))
+      .containsOnly("uuid-1", "uuid-3", "uuid-5", "uuid-6");
+
+    List<String> logs = logTester.logs(LoggerLevel.WARN);
+    assertThat(logs).hasSize(3);
+    assertThat(logs).containsExactlyInAnyOrder(
+      "Global webhook 'webhook-uuid-7' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL if it is still needed.",
+      "Webhook 'webhook-uuid-4' for project 'pn2' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL or ask a project administrator to do it if it is still needed.",
+      "Webhook 'webhook-uuid-2' for project 'pn1' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL or ask a project administrator to do it if it is still needed.");
+  }
+
+  private void insertProject(String uuid, String name) {
+    dbTester.executeInsert("PROJECTS",
+      "NAME", name,
+      "ORGANIZATION_UUID", "default",
+      "KEE", uuid + "-key",
+      "UUID", uuid,
+      "PRIVATE", Boolean.toString(false),
+      "QUALIFIER", "TRK",
+      "UPDATED_AT", System2.INSTANCE.now());
+  }
+
+  private void insertWebhook(String uuid, String url, @Nullable String projectUuid) {
+    dbTester.executeInsert(TABLE_NAME,
+      "UUID", uuid,
+      "NAME", "webhook-" + uuid,
+      "PROJECT_UUID", projectUuid,
+      "URL", url,
+      "CREATED_AT", System2.INSTANCE.now());
+  }
+}
+
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProviderTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProviderTest.java
new file mode 100644 (file)
index 0000000..47274ed
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.platform.db.migration.version.v89.util;
+
+import java.net.SocketException;
+import java.util.List;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NetworkInterfaceProviderTest {
+  private NetworkInterfaceProvider underTest = new NetworkInterfaceProvider();
+
+  @Test
+  public void itGetsListOfNetworkInterfaceAddresses() throws SocketException {
+    assertThat(underTest.getNetworkInterfaceAddresses())
+      .isInstanceOf(List.class)
+      .hasSizeGreaterThan(0);
+  }
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql
new file mode 100644 (file)
index 0000000..de11e9e
--- /dev/null
@@ -0,0 +1,29 @@
+CREATE TABLE "PROJECTS"(
+    "UUID" VARCHAR(40) NOT NULL,
+    "KEE" VARCHAR(400) NOT NULL,
+    "QUALIFIER" VARCHAR(10) NOT NULL,
+    "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+    "NAME" VARCHAR(2000),
+    "DESCRIPTION" VARCHAR(2000),
+    "PRIVATE" BOOLEAN NOT NULL,
+    "TAGS" VARCHAR(500),
+    "CREATED_AT" BIGINT,
+    "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "PROJECTS" ADD CONSTRAINT "PK_NEW_PROJECTS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PROJECTS_KEE" ON "PROJECTS"("KEE");
+CREATE INDEX "IDX_QUALIFIER" ON "PROJECTS"("QUALIFIER");
+
+CREATE TABLE "WEBHOOKS"(
+    "UUID" VARCHAR(40) NOT NULL,
+    "ORGANIZATION_UUID" VARCHAR(40),
+    "PROJECT_UUID" VARCHAR(40),
+    "NAME" VARCHAR(100) NOT NULL,
+    "URL" VARCHAR(2000) NOT NULL,
+    "SECRET" VARCHAR(200),
+    "CREATED_AT" BIGINT NOT NULL,
+    "UPDATED_AT" BIGINT
+);
+ALTER TABLE "WEBHOOKS" ADD CONSTRAINT "PK_WEBHOOKS" PRIMARY KEY("UUID");
+CREATE INDEX "ORGANIZATION_WEBHOOK" ON "WEBHOOKS"("ORGANIZATION_UUID");
+CREATE INDEX "PROJECT_WEBHOOK" ON "WEBHOOKS"("PROJECT_UUID");
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java
new file mode 100644 (file)
index 0000000..a501809
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.webhook;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+@ComputeEngineSide
+public class NetworkInterfaceProvider {
+
+  public List<InetAddress> getNetworkInterfaceAddresses() throws SocketException {
+    return Collections.list(NetworkInterface.getNetworkInterfaces())
+      .stream()
+      .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream())
+      .collect(Collectors.toList());
+  }
+}
index 688cfdb9bd0e996becae332ca6f3e23390385396..71c4448c74707d88b1508f8ebf7208a4a3a738fc 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.webhook;
 
 import java.net.InetAddress;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.Collections;
 import java.util.List;
@@ -36,9 +37,11 @@ import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOO
 public class WebhookCustomDns implements Dns {
 
   private final Configuration configuration;
+  private final NetworkInterfaceProvider networkInterfaceProvider;
 
-  public WebhookCustomDns(Configuration configuration) {
+  public WebhookCustomDns(Configuration configuration, NetworkInterfaceProvider networkInterfaceProvider) {
     this.configuration = configuration;
+    this.networkInterfaceProvider = networkInterfaceProvider;
   }
 
   @NotNull
@@ -46,10 +49,19 @@ public class WebhookCustomDns implements Dns {
   public List<InetAddress> lookup(@NotNull String host) throws UnknownHostException {
     InetAddress address = InetAddress.getByName(host);
     if (configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey()).orElse(true)
-      && (address.isLoopbackAddress() || address.isAnyLocalAddress())) {
+      && (address.isLoopbackAddress() || address.isAnyLocalAddress() || isLocalAddress(address))) {
       throw new IllegalArgumentException("Invalid URL: loopback and wildcard addresses are not allowed for webhooks.");
     }
     return Collections.singletonList(address);
   }
 
+  private boolean isLocalAddress(InetAddress address)  {
+    try {
+      return networkInterfaceProvider.getNetworkInterfaceAddresses().stream()
+        .anyMatch(a -> a != null && a.equals(address));
+    } catch (SocketException e) {
+      throw new IllegalArgumentException("Network interfaces could not be fetched.");
+    }
+  }
+
 }
index 3a515608a06e672145976c52183a4c2d00c6a7b7..97f178eaaaf8ee74ee76d861144ee1fdad12cf05 100644 (file)
@@ -25,6 +25,7 @@ public class WebhookModule extends Module {
   @Override
   protected void configureModule() {
     add(
+      NetworkInterfaceProvider.class,
       WebhookCustomDns.class,
       WebhookCallerImpl.class,
       WebhookDeliveryStorage.class,
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java
new file mode 100644 (file)
index 0000000..be213d0
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.webhook;
+
+import java.net.SocketException;
+import java.util.List;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NetworkInterfaceProviderTest {
+  private NetworkInterfaceProvider underTest = new NetworkInterfaceProvider();
+
+  @Test
+  public void itGetsListOfNetworkInterfaceAddresses() throws SocketException {
+    assertThat(underTest.getNetworkInterfaceAddresses())
+      .isInstanceOf(List.class)
+      .hasSizeGreaterThan(0);
+  }
+}
index be4060c8779bbcec413c6f5829ece4fa636a5cca..885ecae43a6b5cf402b6ccc4bb98542ff2ceb121 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.server.webhook;
 
+import com.google.common.collect.ImmutableList;
+import java.net.InetAddress;
 import java.util.Optional;
 import okhttp3.Credentials;
 import okhttp3.HttpUrl;
@@ -63,6 +65,7 @@ public class WebhookCallerImplTest {
   public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
 
   Configuration configuration = Mockito.mock(Configuration.class);
+  NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class);
 
   private System2 system = new TestSystem2().setNow(NOW);
 
@@ -248,6 +251,29 @@ public class WebhookCallerImplTest {
     assertThat(delivery.getPayload()).isSameAs(PAYLOAD);
   }
 
+  @Test
+  public void silently_catch_error_when_url_is_local_network_interface() throws Exception {
+    String url = "https://192.168.1.21";
+
+    InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse(url).host());
+
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses())
+      .thenReturn(ImmutableList.of(inetAddress));
+
+    Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
+      randomAlphanumeric(40), "my-webhook", url, null);
+
+    WebhookDelivery delivery = newSender(true).call(webhook, PAYLOAD);
+
+    assertThat(delivery.getHttpStatus()).isEmpty();
+    assertThat(delivery.getDurationInMs().get()).isNotNegative();
+    assertThat(delivery.getError().get()).isInstanceOf(IllegalArgumentException.class);
+    assertThat(delivery.getErrorMessage()).contains("Invalid URL: loopback and wildcard addresses are not allowed for webhooks.");
+    assertThat(delivery.getAt()).isEqualTo(NOW);
+    assertThat(delivery.getWebhook()).isSameAs(webhook);
+    assertThat(delivery.getPayload()).isSameAs(PAYLOAD);
+  }
+
   private RecordedRequest takeAndVerifyPostRequest(String expectedPath) throws Exception {
     RecordedRequest request = server.takeRequest();
 
@@ -260,7 +286,7 @@ public class WebhookCallerImplTest {
   private WebhookCaller newSender(boolean validateWebhook) {
     SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER, SonarEdition.COMMUNITY);
     when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(validateWebhook));
-    WebhookCustomDns webhookCustomDns = new WebhookCustomDns(configuration);
+    WebhookCustomDns webhookCustomDns = new WebhookCustomDns(configuration, networkInterfaceProvider);
     return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings().asConfig(), runtime), webhookCustomDns);
   }
 }
index af468b40f3679818695fd692bc0140e71d2753a7..8c6906b9490360b00b4d6cca20480281fba7b8b5 100644 (file)
  */
 package org.sonar.server.webhook;
 
+import com.google.common.collect.ImmutableList;
 import java.net.InetAddress;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.Optional;
+import okhttp3.HttpUrl;
 import org.assertj.core.api.Assertions;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -31,16 +34,19 @@ import static org.mockito.Mockito.when;
 import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOOKS;
 
 public class WebhookCustomDnsTest {
+  private static final String INVALID_URL = "Invalid URL: loopback and wildcard addresses are not allowed for webhooks.";
 
   private Configuration configuration = Mockito.mock(Configuration.class);
-  private WebhookCustomDns underTest = new WebhookCustomDns(configuration);
+  private NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class);
+
+  private WebhookCustomDns underTest = new WebhookCustomDns(configuration, networkInterfaceProvider);
 
   @Test
   public void lookup_fail_on_localhost() {
     when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
 
     Assertions.assertThatThrownBy(() -> underTest.lookup("localhost"))
-      .hasMessageContaining("")
+      .hasMessageContaining(INVALID_URL)
       .isInstanceOf(IllegalArgumentException.class);
   }
 
@@ -49,7 +55,30 @@ public class WebhookCustomDnsTest {
     when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
 
     Assertions.assertThatThrownBy(() -> underTest.lookup("127.0.0.1"))
-      .hasMessageContaining("")
+      .hasMessageContaining(INVALID_URL)
+      .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  @Test
+  public void lookup_fail_on_192_168_1_21() throws UnknownHostException, SocketException {
+    InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse("https://192.168.1.21/").host());
+
+    when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses())
+      .thenReturn(ImmutableList.of(inetAddress));
+
+    Assertions.assertThatThrownBy(() -> underTest.lookup("192.168.1.21"))
+      .hasMessageContaining(INVALID_URL)
+      .isInstanceOf(IllegalArgumentException.class);
+  }
+
+  @Test
+  public void lookup_fail_on_network_interface_throwing_socket_exception() throws SocketException {
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses())
+      .thenThrow(new SocketException());
+
+    Assertions.assertThatThrownBy(() -> underTest.lookup("good-url.com"))
+      .hasMessageContaining("Network interfaces could not be fetched.")
       .isInstanceOf(IllegalArgumentException.class);
   }
 
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java
new file mode 100644 (file)
index 0000000..dfdd636
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.webhook.ws;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class NetworkInterfaceProvider {
+
+  public List<InetAddress> getNetworkInterfaceAddresses() throws SocketException {
+    return Collections.list(NetworkInterface.getNetworkInterfaces())
+      .stream()
+      .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream())
+      .collect(Collectors.toList());
+  }
+}
index 0b94d916723ae90610fdef145b392fb2bd8cd18b..16a1b382853b34645e6521510e6401e15c5f985a 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.webhook.ws;
 
 import java.net.InetAddress;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import okhttp3.HttpUrl;
 import org.sonar.api.config.Configuration;
@@ -34,10 +35,12 @@ public class WebhookSupport {
 
   private final UserSession userSession;
   private final Configuration configuration;
+  private final NetworkInterfaceProvider networkInterfaceProvider;
 
-  public WebhookSupport(UserSession userSession, Configuration configuration) {
+  public WebhookSupport(UserSession userSession, Configuration configuration, NetworkInterfaceProvider networkInterfaceProvider) {
     this.userSession = userSession;
     this.configuration = configuration;
+    this.networkInterfaceProvider = networkInterfaceProvider;
   }
 
   void checkPermission(ProjectDto projectDto) {
@@ -55,13 +58,21 @@ public class WebhookSupport {
         throw new IllegalArgumentException(String.format(message, messageArguments));
       }
       InetAddress address = InetAddress.getByName(okUrl.host());
+
       if (configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey()).orElse(true)
-        && (address.isLoopbackAddress() || address.isAnyLocalAddress())) {
+        && (address.isLoopbackAddress() || address.isAnyLocalAddress() || isLocalAddress(address))) {
         throw new IllegalArgumentException("Invalid URL: loopback and wildcard addresses are not allowed for webhooks.");
       }
     } catch (UnknownHostException e) {
       // if a host can not be resolved the deliveries will fail - no need to block it from being set
       // this will only happen for public URLs
+    } catch (SocketException e) {
+      throw new IllegalStateException("Can not retrieve a network interfaces", e);
     }
   }
+
+  private boolean isLocalAddress(InetAddress address) throws SocketException {
+    return networkInterfaceProvider.getNetworkInterfaceAddresses().stream()
+      .anyMatch(a -> a != null && a.equals(address));
+  }
 }
index a171e44aa8d11ed6997c8129682a3c954fc665b7..1adea825a11e1f4310ad956a8a9eb0844857685e 100644 (file)
@@ -32,6 +32,7 @@ public class WebhooksWsModule extends Module {
       UpdateAction.class,
       DeleteAction.class,
       WebhookDeliveryAction.class,
-      WebhookDeliveriesAction.class);
+      WebhookDeliveriesAction.class,
+      NetworkInterfaceProvider.class);
   }
 }
index 966e6c2c6db80cbb0267b1edc3781c4e04e36d5f..24153d8285f9ad7772c58b14ebb25f289bc54610 100644 (file)
@@ -68,7 +68,8 @@ public class CreateActionTest {
   private final ComponentDbTester componentDbTester = db.components();
   private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
   private final Configuration configuration = mock(Configuration.class);
-  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration);
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider);
   private final ResourceTypes resourceTypes = mock(ResourceTypes.class);
   private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes);
   private final CreateAction underTest = new CreateAction(dbClient, userSession, uuidFactory, webhookSupport, componentFinder);
index 1e779ff10984755497c7623c504defc884d5609c..0b089005b9260d2044eaebc8c7d6b4a5766d7821 100644 (file)
@@ -67,7 +67,8 @@ public class DeleteActionTest {
   private final WebhookDeliveryDao deliveryDao = dbClient.webhookDeliveryDao();
   private final ComponentDbTester componentDbTester = db.components();
   private final Configuration configuration = mock(Configuration.class);
-  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration);
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider);
   private final DeleteAction underTest = new DeleteAction(dbClient, userSession, webhookSupport);
   private final WsActionTester wsActionTester = new WsActionTester(underTest);
 
index bc1115c6995c0de71eca1c1870cd189f956b41b6..0ca1bd2984c9c88ca0139b6f4134195893708754 100644 (file)
@@ -69,7 +69,8 @@ public class ListActionTest {
 
   private final DbClient dbClient = db.getDbClient();
   private final Configuration configuration = mock(Configuration.class);
-  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration);
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider);
   private final ResourceTypes resourceTypes = mock(ResourceTypes.class);
   private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes);
   private final ListAction underTest = new ListAction(dbClient, userSession, webhookSupport, componentFinder);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java
new file mode 100644 (file)
index 0000000..c9b7ba6
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.webhook.ws;
+
+import java.net.SocketException;
+import java.util.List;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class NetworkInterfaceProviderTest {
+  private NetworkInterfaceProvider underTest = new NetworkInterfaceProvider();
+
+  @Test
+  public void itGetsListOfNetworkInterfaceAddresses() throws SocketException {
+    assertThat(underTest.getNetworkInterfaceAddresses())
+      .isInstanceOf(List.class)
+      .hasSizeGreaterThan(0);
+  }
+}
index cbc26e7c90ca4eca89d5eafebe37e2fdb6e21934..0fb45101aadb2427d5e77a9d86801a023dcd99b7 100644 (file)
@@ -63,7 +63,8 @@ public class UpdateActionTest {
   private final WebhookDbTester webhookDbTester = db.webhooks();
   private final ComponentDbTester componentDbTester = db.components();
   private final Configuration configuration = mock(Configuration.class);
-  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration);
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+  private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider);
   private final ResourceTypes resourceTypes = mock(ResourceTypes.class);
   private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes);
   private final UpdateAction underTest = new UpdateAction(dbClient, userSession, webhookSupport, componentFinder);
index 2a5998691abd1cd77b8624122ea377e0095b74c5..2ebc93aa839d0200c0e930aed818289c19a3065c 100644 (file)
  */
 package org.sonar.server.webhook.ws;
 
+import com.google.common.collect.ImmutableList;
 import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import okhttp3.HttpUrl;
+import org.assertj.core.api.Assertions;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.sonar.api.config.Configuration;
 import org.sonar.server.user.UserSession;
 
 import static java.util.Optional.of;
+import static org.assertj.core.api.Assertions.*;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
@@ -36,7 +44,8 @@ import static org.mockito.Mockito.when;
 @RunWith(DataProviderRunner.class)
 public class WebhookSupportTest {
   private final Configuration configuration = mock(Configuration.class);
-  private final WebhookSupport underTest = new WebhookSupport(mock(UserSession.class), configuration);
+  private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class);
+  private final WebhookSupport underTest = new WebhookSupport(mock(UserSession.class), configuration, networkInterfaceProvider);
 
   @DataProvider
   public static Object[][] validUrls() {
@@ -64,9 +73,19 @@ public class WebhookSupportTest {
       {"https://127.0.0.1:7777/some_webhook"},
       {"https://localhost/some_webhook"},
       {"https://localhost:9999/some_webhook"},
+      {"https://192.168.1.21/"},
     };
   }
 
+  @Before
+  public void prepare() throws IOException {
+    InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse("https://192.168.1.21/").host());
+
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses())
+      .thenReturn(ImmutableList.of(inetAddress));
+  }
+
+
   @Test
   @UseDataProvider("validUrls")
   public void checkUrlPatternSuccessfulForValidAddress(String url) {
@@ -88,4 +107,13 @@ public class WebhookSupportTest {
 
     assertThatCode(() -> underTest.checkUrlPattern(url, "msg")).doesNotThrowAnyException();
   }
+
+  @Test
+  public void itThrowsIllegalExceptionIfGettingNetworkInterfaceAddressesFails() throws SocketException {
+    when(networkInterfaceProvider.getNetworkInterfaceAddresses()).thenThrow(new SocketException());
+
+    assertThatThrownBy(() -> underTest.checkUrlPattern("good-url.com", "msg"))
+      .hasMessageContaining("")
+      .isInstanceOf(IllegalArgumentException.class);
+  }
 }
diff --git a/server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644 (file)
index 0000000..1f0955d
--- /dev/null
@@ -0,0 +1 @@
+mock-maker-inline