From 4f3018eb093f9c2d5406df7674eb0e9a7b50e44c Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Tue, 23 May 2017 15:08:17 +0200 Subject: [PATCH] SONAR-9301 Support HTTP redirect on scanner side --- .../sonarqube/ws/client/HttpConnector.java | 79 +++++++++---- .../ws/client/OkHttpClientBuilder.java | 16 ++- .../ws/client/HttpConnectorTest.java | 26 +++++ .../org/sonarqube/tests/Category3Suite.java | 8 +- .../tests/analysis/RedirectTest.java | 107 ++++++++++++++++++ 5 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 tests/src/test/java/org/sonarqube/tests/analysis/RedirectTest.java diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java index 6569eb3535b..9d4c7fdc1ea 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java @@ -40,6 +40,10 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.nullToEmpty; import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT; +import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT; /** * Connect to any SonarQube server available through HTTP or HTTPS. @@ -56,9 +60,9 @@ public class HttpConnector implements WsConnector { * It is required for further usage of {@link HttpUrl#resolve(String)}. */ private final HttpUrl baseUrl; - private final String credentials; private final String systemPassCode; private final OkHttpClient okHttpClient; + private final OkHttpClient noRedirectOkHttpClient; private HttpConnector(Builder builder) { this.baseUrl = HttpUrl.parse(builder.url.endsWith("/") ? builder.url : format("%s/", builder.url)); @@ -67,13 +71,10 @@ public class HttpConnector implements WsConnector { OkHttpClientBuilder okHttpClientBuilder = new OkHttpClientBuilder(); okHttpClientBuilder.setUserAgent(builder.userAgent); - if (isNullOrEmpty(builder.login)) { - // no login nor access token - this.credentials = null; - } else { + if (!isNullOrEmpty(builder.login)) { // password is null when login represents an access token. In this case // the Basic credentials consider an empty password. - this.credentials = Credentials.basic(builder.login, nullToEmpty(builder.password)); + okHttpClientBuilder.setCredentials(Credentials.basic(builder.login, nullToEmpty(builder.password))); } this.systemPassCode = builder.systemPassCode; okHttpClientBuilder.setProxy(builder.proxy); @@ -84,6 +85,14 @@ public class HttpConnector implements WsConnector { okHttpClientBuilder.setSSLSocketFactory(builder.sslSocketFactory); okHttpClientBuilder.setTrustManager(builder.sslTrustManager); this.okHttpClient = okHttpClientBuilder.build(); + this.noRedirectOkHttpClient = newClientWithoutRedirect(this.okHttpClient); + } + + private static OkHttpClient newClientWithoutRedirect(OkHttpClient client) { + return client.newBuilder() + .followRedirects(false) + .followSslRedirects(false) + .build(); } @Override @@ -111,7 +120,7 @@ public class HttpConnector implements WsConnector { completeUrlQueryParameters(getRequest, urlBuilder); Request.Builder okRequestBuilder = prepareOkRequestBuilder(getRequest, urlBuilder).get(); - return doCall(okRequestBuilder.build()); + return new OkHttpResponse(doCall(okHttpClient, okRequestBuilder.build())); } private WsResponse post(PostRequest postRequest) { @@ -141,8 +150,10 @@ public class HttpConnector implements WsConnector { }); body = bodyBuilder.build(); } - Request.Builder reqBuilder = prepareOkRequestBuilder(postRequest, urlBuilder); - return doCall(reqBuilder.post(body).build()); + Request.Builder okRequestBuilder = prepareOkRequestBuilder(postRequest, urlBuilder).post(body); + Response response = doCall(noRedirectOkHttpClient, okRequestBuilder.build()); + response = checkRedirect(response); + return new OkHttpResponse(response); } private HttpUrl.Builder prepareUrlBuilder(WsRequest wsRequest) { @@ -152,7 +163,7 @@ public class HttpConnector implements WsConnector { .newBuilder(); } - private static void completeUrlQueryParameters(BaseRequest request, HttpUrl.Builder urlBuilder) { + private static void completeUrlQueryParameters(BaseRequest request, HttpUrl.Builder urlBuilder) { request.getParameters().getKeys() .forEach(key -> request.getParameters().getValues(key) .forEach(value -> urlBuilder.addQueryParameter(key, value))); @@ -163,27 +174,57 @@ public class HttpConnector implements WsConnector { .url(urlBuilder.build()) .header("Accept", getRequest.getMediaType()) .header("Accept-Charset", "UTF-8"); - if (credentials != null) { - okHttpRequestBuilder.header("Authorization", credentials); - } if (systemPassCode != null) { okHttpRequestBuilder.header("X-Sonar-Passcode", systemPassCode); } - getRequest.getHeaders().getNames().forEach(name -> - okHttpRequestBuilder.header(name, getRequest.getHeaders().getValue(name).get())); + getRequest.getHeaders().getNames().forEach(name -> okHttpRequestBuilder.header(name, getRequest.getHeaders().getValue(name).get())); return okHttpRequestBuilder; } - private OkHttpResponse doCall(Request okRequest) { - Call call = okHttpClient.newCall(okRequest); + private static Response doCall(OkHttpClient client, Request okRequest) { + Call call = client.newCall(okRequest); try { - Response okResponse = call.execute(); - return new OkHttpResponse(okResponse); + return call.execute(); } catch (IOException e) { throw new IllegalStateException("Fail to request " + okRequest.url(), e); } } + private Response checkRedirect(Response response) { + switch (response.code()) { + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_TEMP_REDIRECT: + case HTTP_PERM_REDIRECT: + // OkHttpClient does not follow the redirect with the same HTTP method. A POST is + // redirected to a GET. Because of that the redirect must be manually implemented. + // See: + // https://github.com/square/okhttp/blob/07309c1c7d9e296014268ebd155ebf7ef8679f6c/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java#L316 + // https://github.com/square/okhttp/issues/936#issuecomment-266430151 + return followPostRedirect(response); + default: + return response; + } + } + + private Response followPostRedirect(Response response) { + String location = response.header("Location"); + if (location == null) { + throw new IllegalStateException(format("Missing HTTP header 'Location' in redirect of %s", response.request().url())); + } + HttpUrl url = response.request().url().resolve(location); + + // Don't follow redirects to unsupported protocols. + if (url == null) { + throw new IllegalStateException(format("Unsupported protocol in redirect of %s to %s", response.request().url(), location)); + } + + Request.Builder redirectRequest = response.request().newBuilder(); + redirectRequest.post(response.request().body()); + response.body().close(); + return doCall(noRedirectOkHttpClient, redirectRequest.url(url).build()); + } + /** * @since 5.5 */ diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java index a9d4985fff5..8737a68ea0c 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java @@ -63,6 +63,7 @@ public class OkHttpClientBuilder { private String userAgent; private Proxy proxy; + private String credentials; private String proxyLogin; private String proxyPassword; private long connectTimeoutMs = -1; @@ -137,6 +138,13 @@ public class OkHttpClientBuilder { return this; } + /** + * Set credentials that will be passed on every request + */ + public void setCredentials(String credentials) { + this.credentials = credentials; + } + /** * Sets the default read timeout for new connections. A value of 0 means no timeout. * Default is defined by OkHttp (10 seconds in OkHttp 3.3). @@ -158,7 +166,7 @@ public class OkHttpClientBuilder { if (readTimeoutMs >= 0) { builder.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS); } - builder.addNetworkInterceptor(this::addUserAgent); + builder.addNetworkInterceptor(this::addHeaders); if (proxyLogin != null) { builder.proxyAuthenticator((route, response) -> { if (response.request().header(PROXY_AUTHORIZATION) != null) { @@ -187,11 +195,14 @@ public class OkHttpClientBuilder { return builder.build(); } - private Response addUserAgent(Interceptor.Chain chain) throws IOException { + private Response addHeaders(Interceptor.Chain chain) throws IOException { Request.Builder newRequest = chain.request().newBuilder(); if (userAgent != null) { newRequest.header("User-Agent", userAgent); } + if (credentials != null) { + newRequest.header("Authorization", credentials); + } return chain.proceed(newRequest.build()); } @@ -285,4 +296,5 @@ public class OkHttpClientBuilder { return kmf.getKeyManagers(); } + } diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java index 5c07cd7021b..4e1ca719ac4 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java @@ -20,6 +20,7 @@ package org.sonarqube.ws.client; import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.List; @@ -70,6 +71,31 @@ public class HttpConnectorTest { server.close(); } + @Test + public void follow_redirects_post() throws IOException, InterruptedException { + MockWebServer server2 = new MockWebServer(); + server2.start(); + server2.url("").url().toString(); + + server.enqueue(new MockResponse() + .setResponseCode(302) + .setHeader("Location", server2.url("").url().toString())); + + server2.enqueue(new MockResponse() + .setResponseCode(200)); + + underTest = HttpConnector.newBuilder().url(serverUrl).build(); + PostRequest request = new PostRequest("api/ce/submit").setParam("projectKey", "project"); + WsResponse response = underTest.call(request); + + RecordedRequest recordedRequest = server2.takeRequest(); + + assertThat(recordedRequest.getMethod()).isEqualTo("POST"); + assertThat(recordedRequest.getBody().readUtf8()).isEqualTo("projectKey=project"); + assertThat(response.requestUrl()).isEqualTo(server2.url("").url().toString()); + assertThat(response.code()).isEqualTo(200); + } + @Test public void test_default_settings() throws Exception { answerHelloWorld(); diff --git a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java index 73d44089ac4..51328e715c5 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java @@ -20,6 +20,9 @@ package org.sonarqube.tests; import com.sonar.orchestrator.Orchestrator; +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; import org.sonarqube.tests.analysis.ExtensionLifecycleTest; import org.sonarqube.tests.analysis.FavoriteTest; import org.sonarqube.tests.analysis.IssueJsonReportTest; @@ -28,6 +31,7 @@ import org.sonarqube.tests.analysis.LinksTest; import org.sonarqube.tests.analysis.MultiLanguageTest; import org.sonarqube.tests.analysis.PermissionTest; import org.sonarqube.tests.analysis.ProjectBuilderTest; +import org.sonarqube.tests.analysis.RedirectTest; import org.sonarqube.tests.analysis.ReportDumpTest; import org.sonarqube.tests.analysis.SSLTest; import org.sonarqube.tests.analysis.ScannerTest; @@ -36,9 +40,6 @@ import org.sonarqube.tests.analysis.TempFolderTest; import org.sonarqube.tests.measure.DecimalScaleMetricTest; import org.sonarqube.tests.plugins.VersionPluginTest; import org.sonarqube.tests.webhook.WebhooksTest; -import org.junit.ClassRule; -import org.junit.runner.RunWith; -import org.junit.runners.Suite; import static util.ItUtils.pluginArtifact; import static util.ItUtils.xooPlugin; @@ -60,6 +61,7 @@ import static util.ItUtils.xooPlugin; ReportDumpTest.class, SSLTest.class, FavoriteTest.class, + RedirectTest.class, // measures DecimalScaleMetricTest.class, WebhooksTest.class diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/RedirectTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/RedirectTest.java new file mode 100644 index 00000000000..4e1ffec8f2a --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/analysis/RedirectTest.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.sonarqube.tests.analysis; + +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.BuildResult; +import com.sonar.orchestrator.build.SonarScanner; +import com.sonar.orchestrator.util.NetworkUtils; +import java.net.InetAddress; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.MovedContextHandler; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.sonarqube.tests.Category3Suite; +import org.sonarqube.tests.Tester; +import util.ItUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedirectTest { + + public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR; + public static Tester tester = new Tester(orchestrator).disableOrganizations(); + + @ClassRule + public static RuleChain chain = RuleChain + .outerRule(orchestrator) + .around(tester); + + private static Server server; + private static int redirectPort; + + @BeforeClass + public static void beforeClass() throws Exception { + // enforce scanners to be authenticated + tester.settings().setGlobalSetting("sonar.forceAuthentication", "true"); + + orchestrator.resetData(); + redirectPort = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress()); + + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setMaxThreads(500); + + server = new Server(threadPool); + // HTTP Configuration + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSendServerVersion(true); + httpConfig.setSendDateHeader(false); + + // Moved handler + MovedContextHandler movedContextHandler = new MovedContextHandler(); + movedContextHandler.setPermanent(true); + movedContextHandler.setNewContextURL(orchestrator.getServer().getUrl()); + server.setHandler(movedContextHandler); + + // http connector + ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); + http.setPort(redirectPort); + server.addConnector(http); + server.start(); + } + + @AfterClass + public static void after() throws Exception { + server.stop(); + } + + @Test + public void testFollowRedirectWithAuthentication() { + orchestrator.getServer(); + SonarScanner sonarScanner = SonarScanner.create(ItUtils.projectDir("shared/xoo-sample")) + .setScannerVersion("2.7") + .setProperty("sonar.host.url", "http://localhost:" + redirectPort) + .setProperties( + "sonar.login", com.sonar.orchestrator.container.Server.ADMIN_LOGIN, + "sonar.password", com.sonar.orchestrator.container.Server.ADMIN_PASSWORD); + BuildResult buildResult = orchestrator.executeBuild(sonarScanner); + + // logs show original URL + assertThat(buildResult.getLogs()).contains("ANALYSIS SUCCESSFUL, you can browse " + "http://localhost:" + redirectPort); + + } +} -- 2.39.5