/* * SonarQube * Copyright (C) 2009-2024 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.scanner.http; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; 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.junit.jupiter.api.io.TempDir; import org.junitpioneer.jupiter.RestoreSystemProperties; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.System2; import org.sonar.batch.bootstrapper.EnvironmentInformation; import org.sonar.scanner.bootstrap.GlobalAnalysisMode; import org.sonar.scanner.bootstrap.ScannerProperties; import org.sonar.scanner.bootstrap.SonarUserHome; 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 java.util.Objects.requireNonNull; 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; class ScannerWsClientProviderTest { private static final GlobalAnalysisMode GLOBAL_ANALYSIS_MODE = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())); private static final AnalysisWarnings ANALYSIS_WARNINGS = warning -> { }; @TempDir private Path sonarUserHomeDir; private final SonarUserHome sonarUserHome = mock(SonarUserHome.class); private final Map scannerProps = new HashMap<>(); private final ScannerWsClientProvider underTest = new ScannerWsClientProvider(); private final EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3"); private final System2 system2 = mock(System2.class); private final Properties systemProps = new Properties(); @BeforeEach void configureMocks() { when(system2.properties()).thenReturn(systemProps); when(sonarUserHome.getPath()).thenReturn(sonarUserHomeDir); } @Test void provide_client_with_default_settings() { DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); assertThat(client).isNotNull(); assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/"); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); assertThat(httpConnector.baseUrl()).isEqualTo("http://localhost:9000/"); assertThat(httpConnector.okHttpClient().proxy()).isNull(); assertThat(httpConnector.okHttpClient().connectTimeoutMillis()).isEqualTo(5_000); assertThat(httpConnector.okHttpClient().readTimeoutMillis()).isEqualTo(60_000); } @Test 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(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); assertThat(client).isNotNull(); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/"); assertThat(httpConnector.okHttpClient().proxy()).isNull(); } @Nested class WithMockHttpSonarQube { @RegisterExtension static WireMockExtension sonarqubeMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); @Test void it_should_timeout_on_long_response() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); scannerProps.put("sonar.scanner.responseTimeout", "PT0.2S"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); sonarqubeMock.stubFor(get("/api/plugins/installed") .willReturn(aResponse().withStatus(200) .withFixedDelay(2000) .withBody("Success"))); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); var getRequest = new GetRequest("api/plugins/installed"); var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest)); assertThat(thrown).hasStackTraceContaining("timeout"); } @Test void it_should_timeout_on_slow_response() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); scannerProps.put("sonar.scanner.socketTimeout", "PT0.2S"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); sonarqubeMock.stubFor(get("/api/plugins/installed") .willReturn(aResponse().withStatus(200) .withChunkedDribbleDelay(2, 2000) .withBody("Success"))); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); var getRequest = new GetRequest("api/plugins/installed"); var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest)); assertThat(thrown).hasStackTraceContaining("timeout"); } @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, sonarUserHome)); } @Nested class WithProxy { private static final String PROXY_AUTH_ENABLED = "proxy-auth"; @RegisterExtension static WireMockExtension proxyMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); @BeforeEach void configureMocks(TestInfo info) { 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 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, sonarUserHome); 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 @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, sonarUserHome); 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, sonarUserHome); 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))))); } } } @Nested class WithMockHttpsSonarQube { public static final String KEYSTORE_PWD = "pwdServerP12"; @RegisterExtension static WireMockExtension sonarqubeMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicHttpsPort().httpDisabled(true) .keystoreType("pkcs12") .keystorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server.p12"))).toString()) .keystorePassword(KEYSTORE_PWD) .keyManagerPassword(KEYSTORE_PWD)) .build(); @BeforeEach void mockResponse() { sonarqubeMock.stubFor(get("/api/plugins/installed") .willReturn(aResponse().withStatus(200).withBody("Success"))); } @Test void it_should_not_trust_server_self_signed_certificate_by_default() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); var getRequest = new GetRequest("api/plugins/installed"); var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest)); assertThat(thrown).hasStackTraceContaining("CertificateException"); } @Test void it_should_trust_server_self_signed_certificate_when_certificate_is_in_truststore() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString()); scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); 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"); } } } @Nested class WithMockHttpsSonarQubeAndClientCertificates { public static final String KEYSTORE_PWD = "pwdServerP12"; @RegisterExtension static WireMockExtension sonarqubeMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicHttpsPort().httpDisabled(true) .keystoreType("pkcs12") .keystorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server.p12"))).toString()) .keystorePassword(KEYSTORE_PWD) .keyManagerPassword(KEYSTORE_PWD) .needClientAuth(true) .trustStoreType("pkcs12") .trustStorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server-with-client-ca.p12"))).toString()) .trustStorePassword("pwdServerWithClientCA")) .build(); @BeforeEach void mockResponse() { sonarqubeMock.stubFor(get("/api/plugins/installed") .willReturn(aResponse().withStatus(200).withBody("Success"))); } @Test void it_should_fail_if_client_certificate_not_provided() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString()); scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); HttpConnector httpConnector = (HttpConnector) client.wsConnector(); var getRequest = new GetRequest("api/plugins/installed"); var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest)); assertThat(thrown).satisfiesAnyOf( e -> assertThat(e).hasStackTraceContaining("SSLHandshakeException"), // Exception is flaky because of https://bugs.openjdk.org/browse/JDK-8172163 e -> assertThat(e).hasStackTraceContaining("Broken pipe")); } @Test void it_should_authenticate_using_certificate_in_keystore() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString()); scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA"); scannerProps.put("sonar.scanner.keystorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client.p12"))).toString()); scannerProps.put("sonar.scanner.keystorePassword", "pwdClientCertP12"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); 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"); } } @RestoreSystemProperties @Test void it_should_support_jvm_system_properties() { scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl()); System.setProperty("javax.net.ssl.trustStore", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString()); System.setProperty("javax.net.ssl.trustStorePassword", "pwdClientWithServerCA"); System.setProperty("javax.net.ssl.keyStore", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client.p12"))).toString()); systemProps.setProperty("javax.net.ssl.keyStore", "any value is fine here"); System.setProperty("javax.net.ssl.keyStorePassword", "pwdClientCertP12"); DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome); 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"); } } } private static Path toPath(URL url) { try { return Paths.get(url.toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } } }