From 94d11b4e7035ba66617247ebe027943eb37f914d Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Fri, 12 Apr 2024 17:48:29 +0200 Subject: [PATCH] SONAR-22037 Support new HTTP proxy properties --- .../bootstrap/ScannerWsClientProvider.java | 29 ++- .../ScannerWsClientProviderTest.java | 177 ++++++++++++++++-- 2 files changed, 181 insertions(+), 25 deletions(-) diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java index b06d0eb4e08..76f67edbd49 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java @@ -19,6 +19,8 @@ */ package org.sonar.scanner.bootstrap; +import java.net.InetSocketAddress; +import java.net.Proxy; import org.sonar.api.CoreProperties; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.System2; @@ -30,6 +32,7 @@ import org.springframework.context.annotation.Bean; import static java.lang.Integer.parseInt; import static java.lang.String.valueOf; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_PASSWORD; import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_USER; @@ -57,13 +60,27 @@ public class ScannerWsClientProvider { .url(url) .credentials(login, scannerProps.property(CoreProperties.PASSWORD)); - // OkHttp detect 'http.proxyHost' java property, but credentials should be filled - final String proxyUser = System.getProperty(HTTP_PROXY_USER, ""); - if (!proxyUser.isEmpty()) { - connectorBuilder.proxyCredentials(proxyUser, System.getProperty(HTTP_PROXY_PASSWORD)); + // OkHttp detects 'http.proxyHost' java property already, so just focus on sonar properties + String proxyHost = defaultIfBlank(scannerProps.property("sonar.scanner.proxyHost"), null); + if (proxyHost != null) { + int proxyPort; + String proxyPortStr = defaultIfBlank(scannerProps.property("sonar.scanner.proxyPort"), url.startsWith("https") ? "443" : "80"); + try { + proxyPort = parseInt(proxyPortStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid proxy port: " + proxyPortStr, e); + } + connectorBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort))); } - return new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connectorBuilder.build()), login != null, - globalMode, analysisWarnings); + var scannerProxyUser = scannerProps.property("sonar.scanner.proxyUser"); + String proxyUser = scannerProxyUser != null ? scannerProxyUser : system.properties().getProperty(HTTP_PROXY_USER, ""); + if (isNotBlank(proxyUser)) { + var scannerProxyPwd = scannerProps.property("sonar.scanner.proxyPassword"); + String proxyPassword = scannerProxyPwd != null ? scannerProxyPwd : system.properties().getProperty(HTTP_PROXY_PASSWORD, ""); + connectorBuilder.proxyCredentials(proxyUser, proxyPassword); + } + + return new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connectorBuilder.build()), login != null, globalMode, analysisWarnings); } } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java index 4fcf8036b48..141a3af7670 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java @@ -19,29 +19,85 @@ */ package org.sonar.scanner.bootstrap; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import java.util.Properties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.System2; import org.sonar.batch.bootstrapper.EnvironmentInformation; +import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.HttpConnector; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -public class ScannerWsClientProviderTest { +class ScannerWsClientProviderTest { - private ScannerWsClientProvider underTest = new ScannerWsClientProvider(); - private EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3"); + public static final GlobalAnalysisMode GLOBAL_ANALYSIS_MODE = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())); + public static final AnalysisWarnings ANALYSIS_WARNINGS = warning -> { + }; + private final Map scannerProps = new HashMap<>(); - @Test - public void provide_client_with_default_settings() { - ScannerProperties settings = new ScannerProperties(new HashMap<>()); + private final ScannerWsClientProvider underTest = new ScannerWsClientProvider(); + private final EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3"); + public static final String PROXY_AUTH_ENABLED = "proxy-auth"; + + @RegisterExtension + static WireMockExtension sonarqubeMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .build(); + + @RegisterExtension + static WireMockExtension proxyMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .build(); + + private final System2 system2 = mock(System2.class); + private final Properties systemProps = new Properties(); + + @BeforeEach + void configureMocks(TestInfo info) { + when(system2.properties()).thenReturn(systemProps); - DefaultScannerWsClient client = underTest.provide(settings, env, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), - mock(System2.class),warning -> { - }); + if (info.getTags().contains(PROXY_AUTH_ENABLED)) { + proxyMock.stubFor(get(urlMatching("/api/plugins/.*")) + .inScenario("Proxy Auth") + .whenScenarioStateIs(STARTED) + .willReturn(aResponse() + .withStatus(407) + .withHeader("Proxy-Authenticate", "Basic realm=\"Access to the proxy\"")) + .willSetStateTo("Challenge returned")); + proxyMock.stubFor(get(urlMatching("/api/plugins/.*")) + .inScenario("Proxy Auth") + .whenScenarioStateIs("Challenge returned") + .willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl()))); + } else { + proxyMock.stubFor(get(urlMatching("/api/plugins/.*")).willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl()))); + } + } + + @Test + void provide_client_with_default_settings() { + DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS); assertThat(client).isNotNull(); assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/"); @@ -50,23 +106,106 @@ public class ScannerWsClientProviderTest { assertThat(httpConnector.okHttpClient().proxy()).isNull(); assertThat(httpConnector.okHttpClient().connectTimeoutMillis()).isEqualTo(5_000); assertThat(httpConnector.okHttpClient().readTimeoutMillis()).isEqualTo(60_000); + + // Proxy is not accessed + assertThat(proxyMock.findAllUnmatchedRequests()).isEmpty(); } @Test - public void provide_client_with_custom_settings() { - Map props = new HashMap<>(); - props.put("sonar.host.url", "https://here/sonarqube"); - props.put("sonar.token", "testToken"); - props.put("sonar.ws.timeout", "42"); - ScannerProperties settings = new ScannerProperties(props); + void provide_client_with_custom_settings() { + scannerProps.put("sonar.host.url", "https://here/sonarqube"); + scannerProps.put("sonar.token", "testToken"); + scannerProps.put("sonar.ws.timeout", "42"); - DefaultScannerWsClient client = underTest.provide(settings, env, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), - mock(System2.class),warning -> { - }); + DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS); assertThat(client).isNotNull(); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/"); assertThat(httpConnector.okHttpClient().proxy()).isNull(); } + + @Test + void it_should_honor_scanner_proxy_settings() { + scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); + scannerProps.put("sonar.scanner.proxyHost", "localhost"); + scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort()); + + DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS); + + sonarqubeMock.stubFor(get("/api/plugins/installed") + .willReturn(aResponse().withStatus(200).withBody("Success"))); + + HttpConnector httpConnector = (HttpConnector) client.wsConnector(); + try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) { + assertThat(r.code()).isEqualTo(200); + assertThat(r.content()).isEqualTo("Success"); + } + + proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))); + } + + @Test + void it_should_throw_if_invalid_proxy_port() { + scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); + scannerProps.put("sonar.scanner.proxyHost", "localhost"); + scannerProps.put("sonar.scanner.proxyPort", "not_a_number"); + var scannerPropertiesBean = new ScannerProperties(scannerProps); + + assertThrows(IllegalArgumentException.class, () -> underTest.provide(scannerPropertiesBean, env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS)); + } + + @Test + @Tag(PROXY_AUTH_ENABLED) + void it_should_honor_scanner_proxy_settings_with_auth() { + var proxyLogin = "proxyLogin"; + var proxyPassword = "proxyPassword"; + scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); + scannerProps.put("sonar.scanner.proxyHost", "localhost"); + scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort()); + scannerProps.put("sonar.scanner.proxyUser", proxyLogin); + scannerProps.put("sonar.scanner.proxyPassword", proxyPassword); + + DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS); + + sonarqubeMock.stubFor(get("/api/plugins/installed") + .willReturn(aResponse().withStatus(200).withBody("Success"))); + + HttpConnector httpConnector = (HttpConnector) client.wsConnector(); + try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) { + assertThat(r.code()).isEqualTo(200); + assertThat(r.content()).isEqualTo("Success"); + } + + proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed")) + .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8))))); + + } + + @Test + @Tag(PROXY_AUTH_ENABLED) + void it_should_honor_old_jvm_proxy_auth_properties() { + var proxyLogin = "proxyLogin"; + var proxyPassword = "proxyPassword"; + scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); + scannerProps.put("sonar.scanner.proxyHost", "localhost"); + scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort()); + systemProps.put("http.proxyUser", proxyLogin); + systemProps.put("http.proxyPassword", proxyPassword); + + DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS); + + sonarqubeMock.stubFor(get("/api/plugins/installed") + .willReturn(aResponse().withStatus(200).withBody("Success"))); + + HttpConnector httpConnector = (HttpConnector) client.wsConnector(); + try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) { + assertThat(r.code()).isEqualTo(200); + assertThat(r.content()).isEqualTo("Success"); + } + + proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed")) + .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8))))); + + } } -- 2.39.5