--- /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.UnknownHostException;
+import java.util.Collections;
+import java.util.List;
+import okhttp3.Dns;
+import org.jetbrains.annotations.NotNull;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.server.ServerSide;
+
+import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOOKS;
+
+@ServerSide
+@ComputeEngineSide
+public class WebhookCustomDns implements Dns {
+
+ private final Configuration configuration;
+
+ public WebhookCustomDns(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ @NotNull
+ @Override
+ 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())) {
+ throw new IllegalArgumentException("Invalid URL: loopback and wildcard addresses are not allowed for webhooks.");
+ }
+ return Collections.singletonList(address);
+ }
+
+}
*/
package org.sonar.server.webhook;
+import java.util.Optional;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
+import org.mockito.Mockito;
import org.sonar.api.SonarEdition;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.SonarRuntime;
+import org.sonar.api.config.Configuration;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.internal.SonarRuntimeImpl;
import org.sonar.api.impl.utils.TestSystem2;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOOKS;
public class WebhookCallerImplTest {
@Rule
public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+ Configuration configuration = Mockito.mock(Configuration.class);
+
private System2 system = new TestSystem2().setNow(NOW);
@Test
"my-webhook", server.url("/ping").toString(), null);
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
assertThat(delivery.getHttpStatus()).hasValue(201);
assertThat(delivery.getWebhook().getUuid()).isEqualTo(WEBHOOK_UUID);
- assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0);
+ assertThat(delivery.getDurationInMs().get()).isNotNegative();
assertThat(delivery.getError()).isEmpty();
assertThat(delivery.getAt()).isEqualTo(NOW);
assertThat(delivery.getWebhook()).isSameAs(webhook);
"my-webhook", server.url("/ping").toString(), "my_secret");
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));
- newSender().call(webhook, PAYLOAD);
+ newSender(false).call(webhook, PAYLOAD);
RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getHeader("X-Sonar-Webhook-HMAC-SHA256")).isEqualTo("ef35d3420a3df3d05f8f7eb3b53384abc41395f164245d6c7e78a70e61703dde");
randomAlphanumeric(40), "my-webhook", server.url("/ping").toString(), null);
server.shutdown();
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
assertThat(delivery.getHttpStatus()).isEmpty();
- assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0);
+ assertThat(delivery.getDurationInMs().get()).isNotNegative();
// message can be "Connection refused" or "connect timed out"
assertThat(delivery.getErrorMessage().get()).matches("(.*Connection refused.*)|(.*connect timed out.*)");
assertThat(delivery.getAt()).isEqualTo(NOW);
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", "this_is_not_an_url", null);
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
assertThat(delivery.getHttpStatus()).isEmpty();
- assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0);
+ assertThat(delivery.getDurationInMs().get()).isNotNegative();
assertThat(delivery.getError().get()).isInstanceOf(IllegalArgumentException.class);
- assertThat(delivery.getErrorMessage().get()).isEqualTo("Webhook URL is not valid: this_is_not_an_url");
+ assertThat(delivery.getErrorMessage()).contains("Webhook URL is not valid: this_is_not_an_url");
assertThat(delivery.getAt()).isEqualTo(NOW);
assertThat(delivery.getWebhook()).isSameAs(webhook);
assertThat(delivery.getPayload()).isSameAs(PAYLOAD);
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target")));
server.enqueue(new MockResponse().setResponseCode(200));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
- assertThat(delivery.getHttpStatus().get()).isEqualTo(200);
- assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0);
+ assertThat(delivery.getHttpStatus()).contains(200);
+ assertThat(delivery.getDurationInMs().get()).isNotNegative();
assertThat(delivery.getError()).isEmpty();
assertThat(delivery.getAt()).isEqualTo(NOW);
assertThat(delivery.getWebhook()).isSameAs(webhook);
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target")));
server.enqueue(new MockResponse().setResponseCode(200));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
- assertThat(delivery.getHttpStatus().get()).isEqualTo(200);
+ assertThat(delivery.getHttpStatus()).contains(200);
RecordedRequest redirectedRequest = takeAndVerifyPostRequest("/redirect");
assertThat(redirectedRequest.getHeader("Authorization")).isEqualTo(Credentials.basic(url.username(), url.password()));
server.enqueue(new MockResponse().setResponseCode(307));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
Throwable error = delivery.getError().get();
assertThat(error)
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", "ftp://foo"));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
Throwable error = delivery.getError().get();
assertThat(error)
randomAlphanumeric(40), "my-webhook", url.toString(), null);
server.enqueue(new MockResponse().setBody("pong"));
- WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+ WebhookDelivery delivery = newSender(false).call(webhook, PAYLOAD);
assertThat(delivery.getWebhook().getUrl())
.isEqualTo(url.toString())
assertThat(recordedRequest.getHeader("Authorization")).isEqualTo(Credentials.basic(url.username(), url.password()));
}
+ @Test
+ public void silently_catch_error_when_url_is_localhost(){
+ String url = server.url("/").toString();
+ 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();
return request;
}
- private WebhookCaller newSender() {
+ private WebhookCaller newSender(boolean validateWebhook) {
SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER, SonarEdition.COMMUNITY);
- return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings().asConfig(), runtime));
+ when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(validateWebhook));
+ WebhookCustomDns webhookCustomDns = new WebhookCustomDns(configuration);
+ return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings().asConfig(), runtime), webhookCustomDns);
}
}
--- /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.UnknownHostException;
+import java.util.Optional;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+
+import static org.mockito.Mockito.when;
+import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOOKS;
+
+public class WebhookCustomDnsTest {
+
+ private Configuration configuration = Mockito.mock(Configuration.class);
+ private WebhookCustomDns underTest = new WebhookCustomDns(configuration);
+
+ @Test
+ public void lookup_fail_on_localhost() {
+ when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
+
+ Assertions.assertThatThrownBy(() -> underTest.lookup("localhost"))
+ .hasMessageContaining("")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void lookup_fail_on_127_0_0_1() {
+ when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
+
+ Assertions.assertThatThrownBy(() -> underTest.lookup("127.0.0.1"))
+ .hasMessageContaining("")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void lookup_dont_fail_on_localhost_if_validation_disabled() throws UnknownHostException {
+ when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(false));
+
+ Assertions.assertThat(underTest.lookup("localhost"))
+ .extracting(InetAddress::toString)
+ .containsExactlyInAnyOrder("localhost/127.0.0.1");
+ }
+
+ @Test
+ public void lookup_dont_fail_on_classic_host_with_validation_enabled() throws UnknownHostException {
+ when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true));
+
+ Assertions.assertThat(underTest.lookup("sonarsource.com").toString()).contains("sonarsource.com/");
+ }
+}