package org.sonar.server.computation.task.projectanalysis.webhook;
import java.io.IOException;
+import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import static java.lang.String.format;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT;
import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
@ComputeEngineSide
public class WebhookCallerImpl implements WebhookCaller {
}
private static Request buildHttpRequest(Webhook webhook, WebhookPayload payload) {
+ HttpUrl url = HttpUrl.parse(webhook.getUrl());
+ if (url == null) {
+ throw new IllegalArgumentException("Webhook URL is not valid: " + webhook.getUrl());
+ }
Request.Builder request = new Request.Builder();
- request.url(webhook.getUrl());
+ 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));
+ }
+
RequestBody body = RequestBody.create(JSON, payload.getJson());
request.post(body);
return request.build();
*/
package org.sonar.server.computation.task.projectanalysis.webhook;
+import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
assertThat(delivery.getHttpStatus()).isEmpty();
assertThat(delivery.getDurationInMs()).isEmpty();
assertThat(delivery.getError().get()).isInstanceOf(IllegalArgumentException.class);
- assertThat(delivery.getErrorMessage().get()).isEqualTo("unexpected url: this_is_not_an_url");
+ assertThat(delivery.getErrorMessage().get()).isEqualTo("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);
takeAndVerifyPostRequest("/target");
}
+ @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(PROJECT_UUID, CE_TASK_UUID, "my-webhook", url.toString());
+
+ // /redirect redirects to /target
+ server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target")));
+ server.enqueue(new MockResponse().setResponseCode(200));
+
+ WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+
+ assertThat(delivery.getHttpStatus().get()).isEqualTo(200);
+
+ RecordedRequest redirectedRequest = takeAndVerifyPostRequest("/redirect");
+ assertThat(redirectedRequest.getHeader("Authorization")).isEqualTo(Credentials.basic(url.username(), url.password()));
+
+ RecordedRequest targetRequest = takeAndVerifyPostRequest("/target");
+ assertThat(targetRequest.getHeader("Authorization")).isEqualTo(Credentials.basic(url.username(), url.password()));
+ }
+
@Test
public void redirects_throws_ISE_if_header_Location_is_missing() throws Exception {
HttpUrl url = server.url("/redirect");
.hasMessage("Unsupported protocol in redirect of " + url + " to ftp://foo");
}
- private void takeAndVerifyPostRequest(String expectedPath) throws Exception {
- RecordedRequest redirectedRequest = server.takeRequest();
+ @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(PROJECT_UUID, CE_TASK_UUID, "my-webhook", url.toString());
+ server.enqueue(new MockResponse().setBody("pong"));
+
+ WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
+
+ assertThat(delivery.getWebhook().getUrl())
+ .isEqualTo(url.toString())
+ .contains("://theLogin:thePassword@");
+ RecordedRequest recordedRequest = takeAndVerifyPostRequest("/ping");
+ assertThat(recordedRequest.getHeader("Authorization")).isEqualTo(Credentials.basic(url.username(), url.password()));
+ }
+
+ private RecordedRequest takeAndVerifyPostRequest(String expectedPath) throws Exception {
+ RecordedRequest request = server.takeRequest();
- assertThat(redirectedRequest.getMethod()).isEqualTo("POST");
- assertThat(redirectedRequest.getPath()).isEqualTo(expectedPath);
+ assertThat(request.getMethod()).isEqualTo("POST");
+ assertThat(request.getPath()).isEqualTo(expectedPath);
+ assertThat(request.getHeader("User-Agent")).isEqualTo("SonarQube/6.2");
+ return request;
}
private WebhookCaller newSender() {
private static final String DESCRIPTION = "Webhooks are used to notify external services when a project analysis is done. " +
"An HTTP POST request including a JSON payload is sent to each of the first ten provided URLs. <br/>" +
"Learn more in the <a href=\"https://redirect.sonarsource.com/doc/webhooks.html\">Webhooks documentation</a>.";
+ private static final String URL_DESCRIPTION = "Server endpoint that will receive the webhook payload, for example 'http://my_server/foo'. " +
+ "If HTTP Basic authentication is used, HTTPS is recommended to avoid man in the middle attacks. " +
+ "Example: 'https://myLogin:myPassword@my_server/foo'";
private WebhookProperties() {
// only static stuff
PropertyFieldDefinition.build(URL_FIELD)
.name("URL")
.type(PropertyType.STRING)
+ .description(URL_DESCRIPTION)
.build())
.build(),
PropertyFieldDefinition.build(URL_FIELD)
.name("URL")
.type(PropertyType.STRING)
+ .description(URL_DESCRIPTION)
.build())
.build());
}
+
}