@@ -91,7 +91,8 @@ public class WebHooksImpl implements WebHooks { | |||
@Override | |||
public void sendProjectAnalysisUpdate(Analysis analysis, Supplier<WebhookPayload> payloadSupplier) { | |||
List<Webhook> webhooks = readWebHooksFrom(analysis.getProjectUuid()) | |||
.map(dto -> new Webhook(dto.getUuid(), analysis.getProjectUuid(), analysis.getCeTaskUuid(), analysis.getAnalysisUuid(), dto.getName(), dto.getUrl())) | |||
.map(dto -> new Webhook(dto.getUuid(), analysis.getProjectUuid(), analysis.getCeTaskUuid(), analysis.getAnalysisUuid(), | |||
dto.getName(), dto.getUrl(), dto.getSecret())) | |||
.collect(MoreCollectors.toList()); | |||
if (webhooks.isEmpty()) { | |||
return; |
@@ -37,14 +37,18 @@ public class Webhook { | |||
private final String analysisUuid; | |||
private final String name; | |||
private final String url; | |||
@Nullable | |||
private final String secret; | |||
public Webhook(String uuid, String componentUuid, @Nullable String ceTaskUuid, @Nullable String analysisUuid, String name, String url) { | |||
public Webhook(String uuid, String componentUuid, @Nullable String ceTaskUuid, | |||
@Nullable String analysisUuid, String name, String url, @Nullable String secret) { | |||
this.uuid = uuid; | |||
this.componentUuid = requireNonNull(componentUuid); | |||
this.ceTaskUuid = ceTaskUuid; | |||
this.analysisUuid = analysisUuid; | |||
this.name = requireNonNull(name); | |||
this.url = requireNonNull(url); | |||
this.secret = secret; | |||
} | |||
public String getComponentUuid() { | |||
@@ -70,4 +74,8 @@ public class Webhook { | |||
public Optional<String> getAnalysisUuid() { | |||
return ofNullable(analysisUuid); | |||
} | |||
public Optional<String> getSecret() { | |||
return ofNullable(secret); | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.webhook; | |||
import java.io.IOException; | |||
import java.util.Optional; | |||
import okhttp3.Credentials; | |||
import okhttp3.HttpUrl; | |||
import okhttp3.MediaType; | |||
@@ -27,6 +28,8 @@ import okhttp3.OkHttpClient; | |||
import okhttp3.Request; | |||
import okhttp3.RequestBody; | |||
import okhttp3.Response; | |||
import org.apache.commons.codec.digest.HmacAlgorithms; | |||
import org.apache.commons.codec.digest.HmacUtils; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.utils.System2; | |||
@@ -69,7 +72,7 @@ public class WebhookCallerImpl implements WebhookCaller { | |||
throw new IllegalArgumentException("Webhook URL is not valid: " + webhook.getUrl()); | |||
} | |||
builder.setEffectiveUrl(HttpUrlHelper.obfuscateCredentials(webhook.getUrl(), url)); | |||
Request request = buildHttpRequest(url, payload); | |||
Request request = buildHttpRequest(url, webhook, payload); | |||
try (Response response = execute(request)) { | |||
builder.setHttpStatus(response.code()); | |||
} | |||
@@ -82,13 +85,14 @@ public class WebhookCallerImpl implements WebhookCaller { | |||
.build(); | |||
} | |||
private static Request buildHttpRequest(HttpUrl url, WebhookPayload payload) { | |||
private static Request buildHttpRequest(HttpUrl url, Webhook webhook, WebhookPayload payload) { | |||
Request.Builder request = new Request.Builder(); | |||
request.url(url); | |||
request.header(PROJECT_KEY_HEADER, payload.getProjectKey()); | |||
if (isNotEmpty(url.username())) { | |||
request.header("Authorization", Credentials.basic(url.username(), url.password(), UTF_8)); | |||
} | |||
signatureOf(webhook, payload).ifPresent(signature -> request.header("X-Sonar-Webhook-HMAC-SHA256", signature)); | |||
RequestBody body = RequestBody.create(JSON, payload.getJson()); | |||
request.post(body); | |||
@@ -141,4 +145,9 @@ public class WebhookCallerImpl implements WebhookCaller { | |||
.followSslRedirects(false) | |||
.build(); | |||
} | |||
private static Optional<String> signatureOf(Webhook webhook, WebhookPayload payload) { | |||
return webhook.getSecret() | |||
.map(secret -> new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmacHex(payload.getJson())); | |||
} | |||
} |
@@ -24,7 +24,6 @@ import okhttp3.HttpUrl; | |||
import okhttp3.mockwebserver.MockResponse; | |||
import okhttp3.mockwebserver.MockWebServer; | |||
import okhttp3.mockwebserver.RecordedRequest; | |||
import org.apache.commons.lang.RandomStringUtils; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.DisableOnDebug; | |||
@@ -39,6 +38,7 @@ import org.sonar.api.utils.Version; | |||
import org.sonar.api.utils.internal.TestSystem2; | |||
import org.sonar.server.util.OkHttpClientProvider; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class WebhookCallerImplTest { | |||
@@ -59,8 +59,9 @@ public class WebhookCallerImplTest { | |||
private System2 system = new TestSystem2().setNow(NOW); | |||
@Test | |||
public void send_posts_payload_to_http_server() throws Exception { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/ping").toString()); | |||
public void post_payload_to_http_server() throws Exception { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, randomAlphanumeric(40), | |||
"my-webhook", server.url("/ping").toString(), null); | |||
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); | |||
WebhookDelivery delivery = newSender().call(webhook, PAYLOAD); | |||
@@ -80,11 +81,25 @@ public class WebhookCallerImplTest { | |||
assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2"); | |||
assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo("application/json; charset=utf-8"); | |||
assertThat(recordedRequest.getHeader("X-SonarQube-Project")).isEqualTo(PAYLOAD.getProjectKey()); | |||
assertThat(recordedRequest.getHeader("X-Sonar-Webhook-HMAC-SHA256")).isNull(); | |||
} | |||
@Test | |||
public void sign_payload_if_secret_is_set() throws Exception { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, randomAlphanumeric(40), | |||
"my-webhook", server.url("/ping").toString(), "my_secret"); | |||
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); | |||
newSender().call(webhook, PAYLOAD); | |||
RecordedRequest recordedRequest = server.takeRequest(); | |||
assertThat(recordedRequest.getHeader("X-Sonar-Webhook-HMAC-SHA256")).isEqualTo("ef35d3420a3df3d05f8f7eb3b53384abc41395f164245d6c7e78a70e61703dde"); | |||
} | |||
@Test | |||
public void silently_catch_error_when_external_server_does_not_answer() throws Exception { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/ping").toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", server.url("/ping").toString(), null); | |||
server.shutdown(); | |||
WebhookDelivery delivery = newSender().call(webhook, PAYLOAD); | |||
@@ -100,7 +115,8 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void silently_catch_error_when_url_is_incorrect() { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", "this_is_not_an_url"); | |||
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); | |||
@@ -118,7 +134,8 @@ public class WebhookCallerImplTest { | |||
*/ | |||
@Test | |||
public void redirects_should_be_followed_with_POST_method() throws Exception { | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/redirect").toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", server.url("/redirect").toString(), null); | |||
// /redirect redirects to /target | |||
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target"))); | |||
@@ -140,7 +157,8 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void credentials_are_propagated_to_POST_redirects() throws Exception { | |||
HttpUrl url = server.url("/redirect").newBuilder().username("theLogin").password("thePassword").build(); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", url.toString(), null); | |||
// /redirect redirects to /target | |||
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target"))); | |||
@@ -160,7 +178,8 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void redirects_throws_ISE_if_header_Location_is_missing() { | |||
HttpUrl url = server.url("/redirect"); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", url.toString(), null); | |||
server.enqueue(new MockResponse().setResponseCode(307)); | |||
@@ -175,7 +194,8 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void redirects_throws_ISE_if_header_Location_does_not_relate_to_a_supported_protocol() { | |||
HttpUrl url = server.url("/redirect"); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", url.toString(), null); | |||
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", "ftp://foo")); | |||
@@ -190,7 +210,8 @@ public class WebhookCallerImplTest { | |||
@Test | |||
public void send_basic_authentication_header_if_url_contains_credentials() throws Exception { | |||
HttpUrl url = server.url("/ping").newBuilder().username("theLogin").password("thePassword").build(); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString()); | |||
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, | |||
randomAlphanumeric(40), "my-webhook", url.toString(), null); | |||
server.enqueue(new MockResponse().setBody("pong")); | |||
WebhookDelivery delivery = newSender().call(webhook, PAYLOAD); |
@@ -118,7 +118,7 @@ public class WebhookDeliveryStorageTest { | |||
private static WebhookDelivery.Builder newBuilderTemplate() { | |||
return new WebhookDelivery.Builder() | |||
.setWebhook(new Webhook("WEBHOOK_UUID_1", "COMPONENT1", "TASK1", RandomStringUtils.randomAlphanumeric(40),"Jenkins", "http://jenkins")) | |||
.setWebhook(new Webhook("WEBHOOK_UUID_1", "COMPONENT1", "TASK1", RandomStringUtils.randomAlphanumeric(40),"Jenkins", "http://jenkins", null)) | |||
.setPayload(new WebhookPayload("my-project", "{json}")) | |||
.setAt(1_000_000L) | |||
.setHttpStatus(200) |
@@ -19,60 +19,35 @@ | |||
*/ | |||
package org.sonar.server.webhook; | |||
import java.util.Optional; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class WebhookTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Test | |||
public void constructor_with_null_componentUuid_should_throw_NPE() { | |||
expectedException.expect(NullPointerException.class); | |||
new Webhook(randomAlphanumeric(40), null, null, null, randomAlphanumeric(10), randomAlphanumeric(10)); | |||
} | |||
@Test | |||
public void constructor_with_null_name_should_throw_NPE() { | |||
expectedException.expect(NullPointerException.class); | |||
new Webhook(randomAlphanumeric(40), randomAlphanumeric(10), null, null, null, randomAlphanumeric(10)); | |||
} | |||
@Test | |||
public void constructor_with_null_url_should_throw_NPE() { | |||
expectedException.expect(NullPointerException.class); | |||
new Webhook(randomAlphanumeric(40), randomAlphanumeric(10), null, null, randomAlphanumeric(10), null); | |||
public void webhook_with_only_required_fields() { | |||
Webhook underTest = new Webhook("a_uuid", "a_component_uuid", null, null, "a_name", "an_url", null); | |||
assertThat(underTest.getUuid()).isEqualTo("a_uuid"); | |||
assertThat(underTest.getComponentUuid()).isEqualTo("a_component_uuid"); | |||
assertThat(underTest.getCeTaskUuid()).isEmpty(); | |||
assertThat(underTest.getAnalysisUuid()).isEmpty(); | |||
assertThat(underTest.getName()).isEqualTo("a_name"); | |||
assertThat(underTest.getUrl()).isEqualTo("an_url"); | |||
assertThat(underTest.getSecret()).isEmpty(); | |||
} | |||
@Test | |||
public void constructor_with_null_ceTaskUuid_or_analysisUuidurl_should_return_Optional_empty() { | |||
String componentUuid = randomAlphanumeric(10); | |||
String name = randomAlphanumeric(10); | |||
String url = randomAlphanumeric(10); | |||
Webhook underTest = new Webhook(randomAlphanumeric(40), componentUuid, null, null, name, url); | |||
assertThat(underTest.getComponentUuid()).isEqualTo(componentUuid); | |||
assertThat(underTest.getName()).isEqualTo(name); | |||
assertThat(underTest.getUrl()).isEqualTo(url); | |||
assertThat(underTest.getCeTaskUuid()).isEqualTo(Optional.empty()); | |||
assertThat(underTest.getAnalysisUuid()).isEqualTo(Optional.empty()); | |||
String ceTaskUuid = randomAlphanumeric(10); | |||
String analysisUuid = randomAlphanumeric(10); | |||
underTest = new Webhook(randomAlphanumeric(40), componentUuid, ceTaskUuid, analysisUuid, name, url); | |||
assertThat(underTest.getComponentUuid()).isEqualTo(componentUuid); | |||
assertThat(underTest.getName()).isEqualTo(name); | |||
assertThat(underTest.getUrl()).isEqualTo(url); | |||
assertThat(underTest.getCeTaskUuid().get()).isEqualTo(ceTaskUuid); | |||
assertThat(underTest.getAnalysisUuid().get()).isEqualTo(analysisUuid); | |||
public void webhook_with_all_fields() { | |||
Webhook underTest = new Webhook("a_uuid", "a_component_uuid", "a_task_uuid", "an_analysis", "a_name", "an_url", "a_secret"); | |||
assertThat(underTest.getUuid()).isEqualTo("a_uuid"); | |||
assertThat(underTest.getComponentUuid()).isEqualTo("a_component_uuid"); | |||
assertThat(underTest.getCeTaskUuid()).hasValue("a_task_uuid"); | |||
assertThat(underTest.getAnalysisUuid()).hasValue("an_analysis"); | |||
assertThat(underTest.getName()).isEqualTo("a_name"); | |||
assertThat(underTest.getUrl()).isEqualTo("an_url"); | |||
assertThat(underTest.getSecret()).hasValue("a_secret"); | |||
} | |||
} |