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'
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
// Utility classes
DbPrimaryKeyConstraintFinder.class,
- DropPrimaryKeySqlGenerator.class);
+ DropPrimaryKeySqlGenerator.class,
+ NetworkInterfaceProvider.class);
}
}
@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);
}
}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+}
+
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+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");
--- /dev/null
+/*
+ * 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());
+ }
+}
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;
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
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.");
+ }
+ }
+
}
@Override
protected void configureModule() {
add(
+ NetworkInterfaceProvider.class,
WebhookCustomDns.class,
WebhookCallerImpl.class,
WebhookDeliveryStorage.class,
--- /dev/null
+/*
+ * 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);
+ }
+}
*/
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;
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);
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();
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);
}
}
*/
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;
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);
}
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);
}
--- /dev/null
+/*
+ * 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());
+ }
+}
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;
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) {
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));
+ }
}
UpdateAction.class,
DeleteAction.class,
WebhookDeliveryAction.class,
- WebhookDeliveriesAction.class);
+ WebhookDeliveriesAction.class,
+ NetworkInterfaceProvider.class);
}
}
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);
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);
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);
--- /dev/null
+/*
+ * 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);
+ }
+}
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);
*/
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;
@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() {
{"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) {
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);
+ }
}
--- /dev/null
+mock-maker-inline