diff --git a/its/it-tests/src/test/java/com/sonar/scanner/lib/it/SSLTest.java b/its/it-tests/src/test/java/com/sonar/scanner/lib/it/SSLTest.java index afe83ecf..31b2cdb6 100644 --- a/its/it-tests/src/test/java/com/sonar/scanner/lib/it/SSLTest.java +++ b/its/it-tests/src/test/java/com/sonar/scanner/lib/it/SSLTest.java @@ -115,7 +115,7 @@ private static void startSSLTransparentReverseProxy(boolean requireClientAuth) t // Handler Structure HandlerCollection handlers = new HandlerCollection(); - handlers.setHandlers(new Handler[]{proxyHandler(), new DefaultHandler()}); + handlers.setHandlers(new Handler[] {proxyHandler(), new DefaultHandler()}); server.setHandler(handlers); ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); @@ -227,8 +227,7 @@ public void simple_analysis_with_server_and_without_client_certificate_is_failin p.matches(failedAnalysis + "Caused by: javax\\.net\\.ssl\\.SSLProtocolException: Broken pipe \\(Write failed\\).*") || p.matches(failedAnalysis + "Caused by: javax\\.net\\.ssl\\.SSLHandshakeException: Received fatal alert: bad_certificate.*") || p.matches(failedAnalysis + "Caused by: java\\.net\\.SocketException: Broken pipe.*") || - p.matches(failedAnalysis + "Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.*") - ); + p.matches(failedAnalysis + "Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.*")); } private static Path project(String projectName) { @@ -238,7 +237,8 @@ private static Path project(String projectName) { @Test @UseDataProvider("variousClientTrustStores") public void simple_analysis_with_server_certificate(String clientTrustStore, String keyStorePassword, boolean useJavaSslProperties) throws Exception { - assumeTrue("New SSL properties have been introduced in 10.6", ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10,6) || useJavaSslProperties); + assumeTrue("Support for PKCS12 keystore generated by openssl was added in SQ 10.7", !clientTrustStore.endsWith("-openssl.p12") || + ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 7)); startSSLTransparentReverseProxy(false); SimpleScanner scanner = new SimpleScanner(); diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java b/lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java index bd13681a..fd46423b 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java @@ -19,14 +19,18 @@ */ package org.sonarsource.scanner.lib; +import java.net.InetSocketAddress; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonarsource.scanner.lib.internal.ArchResolver; @@ -36,12 +40,12 @@ import org.sonarsource.scanner.lib.internal.Paths2; import org.sonarsource.scanner.lib.internal.ScannerEngineLauncherFactory; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.HttpConfig; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import org.sonarsource.scanner.lib.internal.util.VersionUtils; import static org.sonarsource.scanner.lib.ScannerProperties.SCANNER_ARCH; import static org.sonarsource.scanner.lib.ScannerProperties.SCANNER_OS; -import static org.sonarsource.scanner.lib.internal.http.ServerConnection.removeTrailingSlash; /** * Entry point to run a Sonar analysis programmatically. @@ -57,14 +61,14 @@ public class ScannerEngineBootstrapper { private final IsolatedLauncherFactory launcherFactory; private final ScannerEngineLauncherFactory scannerEngineLauncherFactory; private final Map bootstrapProperties = new HashMap<>(); - private final ServerConnection serverConnection; + private final ScannerHttpClient scannerHttpClient; private final System2 system; ScannerEngineBootstrapper(String app, String version, System2 system, - ServerConnection serverConnection, IsolatedLauncherFactory launcherFactory, + ScannerHttpClient scannerHttpClient, IsolatedLauncherFactory launcherFactory, ScannerEngineLauncherFactory scannerEngineLauncherFactory) { this.system = system; - this.serverConnection = serverConnection; + this.scannerHttpClient = scannerHttpClient; this.launcherFactory = launcherFactory; this.scannerEngineLauncherFactory = scannerEngineLauncherFactory; this.setBootstrapProperty(InternalProperties.SCANNER_APP, app) @@ -73,7 +77,7 @@ public class ScannerEngineBootstrapper { public static ScannerEngineBootstrapper create(String app, String version) { System2 system = new System2(); - return new ScannerEngineBootstrapper(app, version, system, new ServerConnection(), + return new ScannerEngineBootstrapper(app, version, system, new ScannerHttpClient(), new IsolatedLauncherFactory(), new ScannerEngineLauncherFactory(system)); } @@ -106,20 +110,62 @@ public ScannerEngineFacade bootstrap() { var isSimulation = properties.containsKey(InternalProperties.SCANNER_DUMP_TO_FILE); var sonarUserHome = resolveSonarUserHome(properties); var fileCache = FileCache.create(sonarUserHome); - serverConnection.init(properties, sonarUserHome); + var httpConfig = new HttpConfig(bootstrapProperties, sonarUserHome); + scannerHttpClient.init(httpConfig); String serverVersion = null; if (!isSonarCloud) { - serverVersion = getServerVersion(serverConnection, isSimulation, properties); + serverVersion = getServerVersion(scannerHttpClient, isSimulation, properties); } if (isSimulation) { return new SimulationScannerEngineFacade(properties, isSonarCloud, serverVersion); } else if (isSonarCloud || VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) { - var launcher = scannerEngineLauncherFactory.createLauncher(serverConnection, fileCache, properties); + var launcher = scannerEngineLauncherFactory.createLauncher(scannerHttpClient, fileCache, properties); return new NewScannerEngineFacade(properties, launcher, isSonarCloud, serverVersion); } else { - var launcher = launcherFactory.createLauncher(serverConnection, fileCache); - return new InProcessScannerEngineFacade(properties, launcher, false, serverVersion); + var launcher = launcherFactory.createLauncher(scannerHttpClient, fileCache); + var adaptedProperties = adaptDeprecatedProperties(properties, httpConfig); + return new InProcessScannerEngineFacade(adaptedProperties, launcher, false, serverVersion); + } + } + + /** + * Older SonarQube versions used to rely on some different properties, or even {@link System} properties. + * For backward compatibility, we adapt the new properties to the old format. + */ + @Nonnull + Map adaptDeprecatedProperties(Map properties, HttpConfig httpConfig) { + var adaptedProperties = new HashMap<>(properties); + if (!adaptedProperties.containsKey(HttpConfig.READ_TIMEOUT_SEC_PROPERTY)) { + adaptedProperties.put(HttpConfig.READ_TIMEOUT_SEC_PROPERTY, "" + httpConfig.getSocketTimeout().get(ChronoUnit.SECONDS)); + } + var proxy = httpConfig.getProxy(); + if (proxy != null) { + setSystemPropertyIfNotAlreadySet("http.proxyHost", ((InetSocketAddress) proxy.address()).getHostString()); + setSystemPropertyIfNotAlreadySet("https.proxyHost", ((InetSocketAddress) proxy.address()).getHostString()); + setSystemPropertyIfNotAlreadySet("http.proxyPort", "" + ((InetSocketAddress) proxy.address()).getPort()); + setSystemPropertyIfNotAlreadySet("https.proxyPort", "" + ((InetSocketAddress) proxy.address()).getPort()); + } + setSystemPropertyIfNotAlreadySet("http.proxyUser", httpConfig.getProxyUser()); + setSystemPropertyIfNotAlreadySet("http.proxyPassword", httpConfig.getProxyPassword()); + + var keyStore = httpConfig.getSslConfig().getKeyStore(); + if (keyStore != null) { + setSystemPropertyIfNotAlreadySet("javax.net.ssl.keyStore", keyStore.getPath().toString()); + setSystemPropertyIfNotAlreadySet("javax.net.ssl.keyStorePassword", keyStore.getKeyStorePassword()); + } + var trustStore = httpConfig.getSslConfig().getTrustStore(); + if (trustStore != null) { + setSystemPropertyIfNotAlreadySet("javax.net.ssl.trustStore", trustStore.getPath().toString()); + setSystemPropertyIfNotAlreadySet("javax.net.ssl.trustStorePassword", trustStore.getKeyStorePassword()); + } + + return Map.copyOf(adaptedProperties); + } + + private void setSystemPropertyIfNotAlreadySet(String key, String value) { + if (system.getProperty(key) == null && StringUtils.isNotBlank(value)) { + System.setProperty(key, value); } } @@ -134,16 +180,16 @@ private static Path resolveSonarUserHome(Map properties) { return Paths.get(sonarUserHome); } - private static String getServerVersion(ServerConnection serverConnection, boolean isSimulation, Map properties) { + private static String getServerVersion(ScannerHttpClient scannerHttpClient, boolean isSimulation, Map properties) { if (isSimulation) { return properties.getOrDefault(InternalProperties.SCANNER_VERSION_SIMULATION, "5.6"); } try { - return serverConnection.callRestApi("/analysis/version"); + return scannerHttpClient.callRestApi("/analysis/version"); } catch (Exception e) { try { - return serverConnection.callWebApi("/api/server/version"); + return scannerHttpClient.callWebApi("/api/server/version"); } catch (Exception e2) { var ex = new IllegalStateException("Failed to get server version", e2); ex.addSuppressed(e); @@ -154,8 +200,8 @@ private static String getServerVersion(ServerConnection serverConnection, boolea private void initBootstrapDefaultValues() { setBootstrapPropertyIfNotAlreadySet(ScannerProperties.HOST_URL, getSonarCloudUrl()); - setBootstrapPropertyIfNotAlreadySet(ScannerProperties.API_BASE_URL, isSonarCloud(bootstrapProperties) ? - SONARCLOUD_REST_API : (removeTrailingSlash(bootstrapProperties.get(ScannerProperties.HOST_URL)) + "/api/v2")); + setBootstrapPropertyIfNotAlreadySet(ScannerProperties.API_BASE_URL, + isSonarCloud(bootstrapProperties) ? SONARCLOUD_REST_API : (StringUtils.removeEnd(bootstrapProperties.get(ScannerProperties.HOST_URL), "/") + "/api/v2")); if (!bootstrapProperties.containsKey(SCANNER_OS)) { setBootstrapProperty(SCANNER_OS, new OsResolver(system, new Paths2()).getOs().name().toLowerCase(Locale.ENGLISH)); } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloader.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloader.java index 2160dee0..f21b6d19 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloader.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloader.java @@ -23,15 +23,15 @@ import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; class BootstrapIndexDownloader { private static final Logger LOG = LoggerFactory.getLogger(BootstrapIndexDownloader.class); - private final ServerConnection conn; + private final ScannerHttpClient conn; - BootstrapIndexDownloader(ServerConnection conn) { + BootstrapIndexDownloader(ScannerHttpClient conn) { this.conn = conn; } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/IsolatedLauncherFactory.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/IsolatedLauncherFactory.java index a598a684..e09f5d96 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/IsolatedLauncherFactory.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/IsolatedLauncherFactory.java @@ -32,7 +32,7 @@ import org.sonarsource.scanner.lib.internal.batch.IsolatedLauncher; import org.sonarsource.scanner.lib.internal.cache.CachedFile; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; public class IsolatedLauncherFactory { @@ -61,11 +61,11 @@ private IsolatedClassloader createClassLoader(List jarFiles, ClassloadRule return classloader; } - public IsolatedLauncherAndClassloader createLauncher(ServerConnection serverConnection, FileCache fileCache) { + public IsolatedLauncherAndClassloader createLauncher(ScannerHttpClient scannerHttpClient, FileCache fileCache) { Set unmaskRules = new HashSet<>(); unmaskRules.add("org.sonarsource.scanner.lib.internal.batch."); ClassloadRules rules = new ClassloadRules(Collections.emptySet(), unmaskRules); - LegacyScannerEngineDownloader legacyScannerEngineDownloader = new LegacyScannerEngineDownloaderFactory(serverConnection, fileCache).create(); + LegacyScannerEngineDownloader legacyScannerEngineDownloader = new LegacyScannerEngineDownloaderFactory(scannerHttpClient, fileCache).create(); return createLauncher(legacyScannerEngineDownloader, rules); } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactory.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactory.java index b44c9759..7517fabf 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactory.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactory.java @@ -46,7 +46,7 @@ import org.sonarsource.scanner.lib.internal.cache.CachedFile; import org.sonarsource.scanner.lib.internal.cache.FileCache; import org.sonarsource.scanner.lib.internal.cache.HashMismatchException; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import org.sonarsource.scanner.lib.internal.util.CompressionUtils; import static java.lang.String.format; @@ -72,7 +72,7 @@ public JavaRunnerFactory(System2 system, ProcessWrapperFactory processWrapperFac this.processWrapperFactory = processWrapperFactory; } - public JavaRunner createRunner(ServerConnection serverConnection, FileCache fileCache, Map properties) { + public JavaRunner createRunner(ScannerHttpClient scannerHttpClient, FileCache fileCache, Map properties) { String javaExecutablePropValue = properties.get(JAVA_EXECUTABLE_PATH); if (javaExecutablePropValue != null) { LOG.info("Using the configured java executable '{}'", javaExecutablePropValue); @@ -82,7 +82,7 @@ public JavaRunner createRunner(ServerConnection serverConnection, FileCache file if (skipJreProvisioning) { LOG.info("JRE provisioning is disabled"); } else { - var cachedFile = getJreFromServer(serverConnection, fileCache, properties, true); + var cachedFile = getJreFromServer(scannerHttpClient, fileCache, properties, true); if (cachedFile.isPresent()) { return new JavaRunner(cachedFile.get().getPathInCache(), cachedFile.get().isCacheHit() ? JreCacheHit.HIT : JreCacheHit.MISS); } @@ -129,34 +129,34 @@ private Path findJavaInPath(String javaExe) { } } - private static Optional getJreFromServer(ServerConnection serverConnection, FileCache fileCache, Map properties, boolean retry) { + private static Optional getJreFromServer(ScannerHttpClient scannerHttpClient, FileCache fileCache, Map properties, boolean retry) { String os = properties.get(SCANNER_OS); String arch = properties.get(SCANNER_ARCH); LOG.info("JRE provisioning: os[{}], arch[{}]", os, arch); try { - var jreMetadata = getJreMetadata(serverConnection, os, arch); + var jreMetadata = getJreMetadata(scannerHttpClient, os, arch); if (jreMetadata.isEmpty()) { LOG.info("No JRE found for this OS/architecture"); return Optional.empty(); } var cachedFile = fileCache.getOrDownload(jreMetadata.get().getFilename(), jreMetadata.get().getSha256(), "SHA-256", - new JreDownloader(serverConnection, jreMetadata.get())); + new JreDownloader(scannerHttpClient, jreMetadata.get())); var extractedDirectory = extractArchive(cachedFile.getPathInCache()); return Optional.of(new CachedFile(extractedDirectory.resolve(jreMetadata.get().javaPath), cachedFile.isCacheHit())); } catch (HashMismatchException e) { if (retry) { // A new JRE might have been published between the metadata fetch and the download LOG.warn("Failed to get the JRE, retrying..."); - return getJreFromServer(serverConnection, fileCache, properties, false); + return getJreFromServer(scannerHttpClient, fileCache, properties, false); } throw e; } } - private static Optional getJreMetadata(ServerConnection serverConnection, String os, String arch) { + private static Optional getJreMetadata(ScannerHttpClient scannerHttpClient, String os, String arch) { try { - String response = serverConnection.callRestApi(format(API_PATH_JRE + "?os=%s&arch=%s", os, arch)); + String response = scannerHttpClient.callRestApi(format(API_PATH_JRE + "?os=%s&arch=%s", os, arch)); Type listType = new TypeToken>() { }.getType(); List jres = new Gson().fromJson(response, listType); @@ -239,10 +239,10 @@ private static void extract(Path compressedFile, Path targetDir) throws IOExcept } static class JreDownloader implements FileCache.Downloader { - private final ServerConnection connection; + private final ScannerHttpClient connection; private final JreMetadata jreMetadata; - JreDownloader(ServerConnection connection, JreMetadata jreMetadata) { + JreDownloader(ScannerHttpClient connection, JreMetadata jreMetadata) { this.connection = connection; this.jreMetadata = jreMetadata; } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloader.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloader.java index 6be9d801..363cf4cc 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloader.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloader.java @@ -30,7 +30,7 @@ import org.sonarsource.scanner.lib.internal.BootstrapIndexDownloader.JarEntry; import org.sonarsource.scanner.lib.internal.cache.CachedFile; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static java.lang.String.format; @@ -70,9 +70,9 @@ private List getOrDownloadScannerEngineFiles() { } static class ScannerFileDownloader implements FileCache.Downloader { - private final ServerConnection connection; + private final ScannerHttpClient connection; - ScannerFileDownloader(ServerConnection conn) { + ScannerFileDownloader(ScannerHttpClient conn) { this.connection = conn; } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactory.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactory.java index 660cd220..6465342f 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactory.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactory.java @@ -20,20 +20,20 @@ package org.sonarsource.scanner.lib.internal; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; class LegacyScannerEngineDownloaderFactory { - private final ServerConnection serverConnection; + private final ScannerHttpClient scannerHttpClient; private final FileCache fileCache; - LegacyScannerEngineDownloaderFactory(ServerConnection conn, FileCache fileCache) { - this.serverConnection = conn; + LegacyScannerEngineDownloaderFactory(ScannerHttpClient conn, FileCache fileCache) { + this.scannerHttpClient = conn; this.fileCache = fileCache; } LegacyScannerEngineDownloader create() { - BootstrapIndexDownloader bootstrapIndexDownloader = new BootstrapIndexDownloader(serverConnection); - LegacyScannerEngineDownloader.ScannerFileDownloader scannerFileDownloader = new LegacyScannerEngineDownloader.ScannerFileDownloader(serverConnection); + BootstrapIndexDownloader bootstrapIndexDownloader = new BootstrapIndexDownloader(scannerHttpClient); + LegacyScannerEngineDownloader.ScannerFileDownloader scannerFileDownloader = new LegacyScannerEngineDownloader.ScannerFileDownloader(scannerHttpClient); JarExtractor jarExtractor = new JarExtractor(); return new LegacyScannerEngineDownloader(scannerFileDownloader, bootstrapIndexDownloader, fileCache, jarExtractor); } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactory.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactory.java index 6110ea7e..43f166aa 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactory.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactory.java @@ -32,7 +32,7 @@ import org.sonarsource.scanner.lib.internal.cache.CachedFile; import org.sonarsource.scanner.lib.internal.cache.FileCache; import org.sonarsource.scanner.lib.internal.cache.HashMismatchException; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; public class ScannerEngineLauncherFactory { @@ -49,10 +49,10 @@ public ScannerEngineLauncherFactory(System2 system) { this.javaRunnerFactory = javaRunnerFactory; } - public ScannerEngineLauncher createLauncher(ServerConnection serverConnection, FileCache fileCache, Map properties) { - JavaRunner javaRunner = javaRunnerFactory.createRunner(serverConnection, fileCache, properties); + public ScannerEngineLauncher createLauncher(ScannerHttpClient scannerHttpClient, FileCache fileCache, Map properties) { + JavaRunner javaRunner = javaRunnerFactory.createRunner(scannerHttpClient, fileCache, properties); jreSanityCheck(javaRunner); - var scannerEngine = getScannerEngine(serverConnection, fileCache, true); + var scannerEngine = getScannerEngine(scannerHttpClient, fileCache, true); return new ScannerEngineLauncher(javaRunner, scannerEngine); } @@ -60,24 +60,24 @@ private static void jreSanityCheck(JavaRunner javaRunner) { javaRunner.execute(Collections.singletonList("--version"), null, LOG::debug); } - private static CachedFile getScannerEngine(ServerConnection serverConnection, FileCache fileCache, boolean retry) { + private static CachedFile getScannerEngine(ScannerHttpClient scannerHttpClient, FileCache fileCache, boolean retry) { try { - var scannerEngineMetadata = getScannerEngineMetadata(serverConnection); + var scannerEngineMetadata = getScannerEngineMetadata(scannerHttpClient); return fileCache.getOrDownload(scannerEngineMetadata.getFilename(), scannerEngineMetadata.getSha256(), "SHA-256", - new ScannerEngineDownloader(serverConnection, scannerEngineMetadata)); + new ScannerEngineDownloader(scannerHttpClient, scannerEngineMetadata)); } catch (HashMismatchException e) { if (retry) { // A new scanner-engine might have been published between the metadata fetch and the download LOG.warn("Failed to get the scanner-engine, retrying..."); - return getScannerEngine(serverConnection, fileCache, false); + return getScannerEngine(scannerHttpClient, fileCache, false); } throw e; } } - private static ScannerEngineMetadata getScannerEngineMetadata(ServerConnection serverConnection) { + private static ScannerEngineMetadata getScannerEngineMetadata(ScannerHttpClient scannerHttpClient) { try { - String response = serverConnection.callRestApi(API_PATH_ENGINE); + String response = scannerHttpClient.callRestApi(API_PATH_ENGINE); return new Gson().fromJson(response, ScannerEngineMetadata.class); } catch (IOException e) { throw new IllegalStateException("Failed to get scanner-engine metadata", e); @@ -91,10 +91,10 @@ public ScannerEngineMetadata(String filename, String sha256, @Nullable String do } static class ScannerEngineDownloader implements FileCache.Downloader { - private final ServerConnection connection; + private final ScannerHttpClient connection; private final ScannerEngineMetadata scannerEngineMetadata; - ScannerEngineDownloader(ServerConnection connection, ScannerEngineMetadata scannerEngineMetadata) { + ScannerEngineDownloader(ScannerHttpClient connection, ScannerEngineMetadata scannerEngineMetadata) { this.connection = connection; this.scannerEngineMetadata = scannerEngineMetadata; } diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/HttpConfig.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/HttpConfig.java new file mode 100644 index 00000000..23b6af74 --- /dev/null +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/HttpConfig.java @@ -0,0 +1,254 @@ +/* + * SonarScanner Java Library + * Copyright (C) 2011-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.sonarsource.scanner.lib.internal.http; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonarsource.scanner.lib.ScannerProperties; +import org.sonarsource.scanner.lib.internal.InternalProperties; +import org.sonarsource.scanner.lib.internal.http.ssl.CertificateStore; +import org.sonarsource.scanner.lib.internal.http.ssl.SslConfig; + +import static java.lang.Integer.parseInt; +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_CONNECT_TIMEOUT; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_KEYSTORE_PASSWORD; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_KEYSTORE_PATH; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_HOST; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_PASSWORD; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_PORT; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_USER; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_RESPONSE_TIMEOUT; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_SOCKET_TIMEOUT; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_TRUSTSTORE_PASSWORD; +import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_TRUSTSTORE_PATH; + +public class HttpConfig { + + private static final Logger LOG = LoggerFactory.getLogger(HttpConfig.class); + + static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(5); + static final Duration DEFAULT_RESPONSE_TIMEOUT = Duration.ZERO; + public static final String READ_TIMEOUT_SEC_PROPERTY = "sonar.ws.timeout"; + static final Duration DEFAULT_READ_TIMEOUT_SEC = Duration.ofSeconds(60); + static final int DEFAULT_PROXY_PORT = 80; + + private final String webApiBaseUrl; + private final String restApiBaseUrl; + @Nullable + private final String token; + @Nullable + private final String login; + @Nullable + private final String password; + + private final SslConfig sslConfig; + private final Duration socketTimeout; + private final Duration connectTimeout; + private final Duration responseTimeout; + @Nullable + private final Proxy proxy; + private final String proxyUser; + private final String proxyPassword; + private final String userAgent; + + public HttpConfig(Map bootstrapProperties, Path sonarUserHome) { + this.webApiBaseUrl = StringUtils.removeEnd(bootstrapProperties.get(ScannerProperties.HOST_URL), "/"); + this.restApiBaseUrl = StringUtils.removeEnd(bootstrapProperties.get(ScannerProperties.API_BASE_URL), "/"); + this.token = bootstrapProperties.get(ScannerProperties.SONAR_TOKEN); + this.login = bootstrapProperties.get(ScannerProperties.SONAR_LOGIN); + this.password = bootstrapProperties.get(ScannerProperties.SONAR_PASSWORD); + this.userAgent = format("%s/%s", bootstrapProperties.get(InternalProperties.SCANNER_APP), bootstrapProperties.get(InternalProperties.SCANNER_APP_VERSION)); + this.socketTimeout = loadDuration(bootstrapProperties, SONAR_SCANNER_SOCKET_TIMEOUT, READ_TIMEOUT_SEC_PROPERTY, DEFAULT_READ_TIMEOUT_SEC); + this.connectTimeout = loadDuration(bootstrapProperties, SONAR_SCANNER_CONNECT_TIMEOUT, null, DEFAULT_CONNECT_TIMEOUT); + this.responseTimeout = loadDuration(bootstrapProperties, SONAR_SCANNER_RESPONSE_TIMEOUT, null, DEFAULT_RESPONSE_TIMEOUT); + this.sslConfig = loadSslConfig(bootstrapProperties, sonarUserHome); + this.proxy = loadProxy(bootstrapProperties); + this.proxyUser = loadProxyUser(bootstrapProperties); + this.proxyPassword = loadProxyPassword(bootstrapProperties); + } + + private static String loadProxyPassword(Map bootstrapProperties) { + var scannerProxyPwd = bootstrapProperties.get(SONAR_SCANNER_PROXY_PASSWORD); + return scannerProxyPwd != null ? scannerProxyPwd : System.getProperty("http.proxyPassword", ""); + } + + private static String loadProxyUser(Map bootstrapProperties) { + var scannerProxyUser = bootstrapProperties.get(SONAR_SCANNER_PROXY_USER); + return scannerProxyUser != null ? scannerProxyUser : System.getProperty("http.proxyUser", ""); + } + + private static Duration loadDuration(Map bootstrapProperties, String propKey, @Nullable String deprecatedPropKey, Duration defaultValue) { + if (bootstrapProperties.containsKey(propKey)) { + return parseDurationProperty(bootstrapProperties.get(propKey), propKey); + } else if (deprecatedPropKey != null && bootstrapProperties.containsKey(deprecatedPropKey)) { + LOG.warn("Property {} is deprecated and will be removed in a future version. Please use {} instead.", deprecatedPropKey, propKey); + return parseDurationProperty(bootstrapProperties.get(deprecatedPropKey), deprecatedPropKey); + } else { + return defaultValue; + } + } + + @Nullable + private static Proxy loadProxy(Map bootstrapProperties) { + // OkHttp detects 'http.proxyHost' java property already, so just focus on sonar properties + String proxyHost = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_PROXY_HOST), null); + if (proxyHost != null) { + int proxyPort; + if (bootstrapProperties.containsKey(SONAR_SCANNER_PROXY_PORT)) { + proxyPort = parseIntProperty(bootstrapProperties.get(SONAR_SCANNER_PROXY_PORT), SONAR_SCANNER_PROXY_PORT); + } else { + proxyPort = DEFAULT_PROXY_PORT; + } + return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } else { + return null; + } + } + + /** + * For testing, we can accept timeouts that are smaller than a second, expressed using ISO-8601 format for durations. + * If we can't parse as ISO-8601, then fallback to the official format that is simply the number of seconds + */ + private static Duration parseDurationProperty(String propValue, String propKey) { + try { + return Duration.parse(propValue); + } catch (DateTimeParseException e) { + return Duration.ofSeconds(parseIntProperty(propValue, propKey)); + } + } + + private static int parseIntProperty(String propValue, String propKey) { + try { + return parseInt(propValue); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(propKey + " is not a valid integer: " + propValue, e); + } + } + + private static SslConfig loadSslConfig(Map bootstrapProperties, Path sonarUserHome) { + var keyStore = loadKeyStoreConfig(bootstrapProperties, sonarUserHome); + var trustStore = loadTrustStoreConfig(bootstrapProperties, sonarUserHome); + return new SslConfig(keyStore, trustStore); + } + + @Nullable + private static CertificateStore loadTrustStoreConfig(Map bootstrapProperties, Path sonarUserHome) { + var trustStorePath = parseFileProperty(bootstrapProperties, SONAR_SCANNER_TRUSTSTORE_PATH, "truststore", sonarUserHome.resolve("ssl/truststore.p12")); + if (trustStorePath != null) { + LOG.debug("Using truststore: {}", trustStorePath); + var trustStorePassword = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_TRUSTSTORE_PASSWORD), CertificateStore.DEFAULT_PASSWORD); + return new CertificateStore(trustStorePath, trustStorePassword); + } + return null; + } + + @Nullable + private static CertificateStore loadKeyStoreConfig(Map bootstrapProperties, Path sonarUserHome) { + var keyStorePath = parseFileProperty(bootstrapProperties, SONAR_SCANNER_KEYSTORE_PATH, "keystore", sonarUserHome.resolve("ssl/keystore.p12")); + if (keyStorePath != null) { + LOG.debug("Using keystore: {}", keyStorePath); + var keyStorePassword = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_KEYSTORE_PASSWORD), CertificateStore.DEFAULT_PASSWORD); + return new CertificateStore(keyStorePath, keyStorePassword); + } + return null; + } + + @Nullable + private static Path parseFileProperty(Map bootstrapProperties, String propKey, String labelForLogs, Path defaultPath) { + if (bootstrapProperties.containsKey(propKey)) { + var keyStorePath = Paths.get(bootstrapProperties.get(propKey)); + if (!Files.exists(keyStorePath)) { + throw new IllegalArgumentException("The " + labelForLogs + " file does not exist: " + keyStorePath); + } + return keyStorePath; + } else if (Files.isRegularFile(defaultPath)) { + return defaultPath; + } + return null; + } + + public String getWebApiBaseUrl() { + return webApiBaseUrl; + } + + public String getRestApiBaseUrl() { + return restApiBaseUrl; + } + + @Nullable + public String getToken() { + return token; + } + + @Nullable + public String getLogin() { + return login; + } + + @Nullable + public String getPassword() { + return password; + } + + public String getUserAgent() { + return userAgent; + } + + public SslConfig getSslConfig() { + return sslConfig; + } + + public Duration getConnectTimeout() { + return connectTimeout; + } + + public Duration getResponseTimeout() { + return responseTimeout; + } + + public Duration getSocketTimeout() { + return socketTimeout; + } + + @Nullable + public Proxy getProxy() { + return proxy; + } + + public String getProxyUser() { + return proxyUser; + } + + public String getProxyPassword() { + return proxyPassword; + } +} diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactory.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactory.java index f8b19720..ade4de72 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactory.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactory.java @@ -23,15 +23,10 @@ import java.net.CookieManager; import java.net.CookiePolicy; import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.Proxy; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.KeyStore; -import java.time.Duration; -import java.time.format.DateTimeParseException; -import java.util.Map; import java.util.concurrent.TimeUnit; import nl.altindag.ssl.SSLFactory; import nl.altindag.ssl.exception.GenericKeyStoreException; @@ -43,27 +38,11 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sonarsource.scanner.lib.ScannerProperties; -import org.sonarsource.scanner.lib.internal.http.ssl.CertificateStore; import org.sonarsource.scanner.lib.internal.http.ssl.SslConfig; -import static java.lang.Integer.parseInt; -import static java.lang.String.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_CONNECT_TIMEOUT; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_KEYSTORE_PASSWORD; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_KEYSTORE_PATH; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_HOST; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_PASSWORD; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_PORT; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_PROXY_USER; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_RESPONSE_TIMEOUT; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_SOCKET_TIMEOUT; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_TRUSTSTORE_PASSWORD; -import static org.sonarsource.scanner.lib.ScannerProperties.SONAR_SCANNER_TRUSTSTORE_PATH; public class OkHttpClientFactory { @@ -74,11 +53,6 @@ public class OkHttpClientFactory { // use the same cookie jar for all instances private static final JavaNetCookieJar COOKIE_JAR; - static final int DEFAULT_CONNECT_TIMEOUT = 5; - static final int DEFAULT_RESPONSE_TIMEOUT = 0; - static final String READ_TIMEOUT_SEC_PROPERTY = "sonar.ws.timeout"; - static final int DEFAULT_READ_TIMEOUT_SEC = 60; - private OkHttpClientFactory() { // only statics } @@ -89,18 +63,14 @@ private OkHttpClientFactory() { COOKIE_JAR = new JavaNetCookieJar(COOKIE_MANAGER); } - static OkHttpClient create(Map bootstrapProperties, Path sonarUserHome) { + static OkHttpClient create(HttpConfig httpConfig) { - String oldSocketTimeout = defaultIfBlank(bootstrapProperties.get(READ_TIMEOUT_SEC_PROPERTY), valueOf(DEFAULT_READ_TIMEOUT_SEC)); - String socketTimeout = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_SOCKET_TIMEOUT), oldSocketTimeout); - String connectTimeout = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_CONNECT_TIMEOUT), valueOf(DEFAULT_CONNECT_TIMEOUT)); - String responseTimeout = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_RESPONSE_TIMEOUT), valueOf(DEFAULT_RESPONSE_TIMEOUT)); - var sslContext = configureSsl(parseSslConfig(bootstrapProperties, sonarUserHome)); + var sslContext = configureSsl(httpConfig.getSslConfig()); OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() - .connectTimeout(parseDurationProperty(connectTimeout, SONAR_SCANNER_CONNECT_TIMEOUT), TimeUnit.MILLISECONDS) - .readTimeout(parseDurationProperty(socketTimeout, SONAR_SCANNER_SOCKET_TIMEOUT), TimeUnit.MILLISECONDS) - .callTimeout(parseDurationProperty(responseTimeout, SONAR_SCANNER_RESPONSE_TIMEOUT), TimeUnit.MILLISECONDS) + .connectTimeout(httpConfig.getConnectTimeout().toMillis(), TimeUnit.MILLISECONDS) + .readTimeout(httpConfig.getSocketTimeout().toMillis(), TimeUnit.MILLISECONDS) + .callTimeout(httpConfig.getResponseTimeout().toMillis(), TimeUnit.MILLISECONDS) .cookieJar(COOKIE_JAR) .sslSocketFactory(sslContext.getSslSocketFactory(), sslContext.getTrustManager().orElseThrow()); @@ -110,27 +80,18 @@ static OkHttpClient create(Map bootstrapProperties, Path sonarUs .build(); okHttpClientBuilder.connectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT)); - // OkHttp detects 'http.proxyHost' java property already, so just focus on sonar properties - String proxyHost = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_PROXY_HOST), null); - if (proxyHost != null) { - var defaultProxyPort = bootstrapProperties.get(ScannerProperties.HOST_URL).startsWith("https") ? "443" : "80"; - String proxyPortStr = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_PROXY_PORT), defaultProxyPort); - var proxyPort = parseIntProperty(proxyPortStr, SONAR_SCANNER_PROXY_PORT); - okHttpClientBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort))); + if (httpConfig.getProxy() != null) { + okHttpClientBuilder.proxy(httpConfig.getProxy()); } - var scannerProxyUser = bootstrapProperties.get(SONAR_SCANNER_PROXY_USER); - String proxyUser = scannerProxyUser != null ? scannerProxyUser : System.getProperty("http.proxyUser", ""); - if (isNotBlank(proxyUser)) { - var scannerProxyPwd = bootstrapProperties.get(SONAR_SCANNER_PROXY_PASSWORD); - String proxyPassword = scannerProxyPwd != null ? scannerProxyPwd : System.getProperty("http.proxyPassword", ""); + if (isNotBlank(httpConfig.getProxyUser())) { okHttpClientBuilder.proxyAuthenticator((route, response) -> { if (response.request().header(PROXY_AUTHORIZATION) != null) { // Give up, we've already attempted to authenticate. return null; } if (HttpURLConnection.HTTP_PROXY_AUTH == response.code()) { - String credential = Credentials.basic(proxyUser, proxyPassword, UTF_8); + String credential = Credentials.basic(httpConfig.getProxyUser(), httpConfig.getProxyPassword(), UTF_8); return response.request().newBuilder().header(PROXY_AUTHORIZATION, credential).build(); } return null; @@ -144,36 +105,6 @@ static OkHttpClient create(Map bootstrapProperties, Path sonarUs return okHttpClientBuilder.build(); } - /** - * For testing, we can accept timeouts that are smaller than a second, expressed using ISO-8601 format for durations. - * If we can't parse as ISO-8601, then fallback to the official format that is simply the number of seconds - */ - private static int parseDurationProperty(String propValue, String propKey) { - try { - return (int) Duration.parse(propValue).toMillis(); - } catch (DateTimeParseException e) { - return parseIntProperty(propValue, propKey) * 1_000; - } - } - - private static int parseIntProperty(String propValue, String propKey) { - try { - return parseInt(propValue); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(propKey + " is not a valid integer: " + propValue, e); - } - } - - private static SslConfig parseSslConfig(Map bootstrapProperties, Path sonarUserHome) { - var keyStorePath = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_KEYSTORE_PATH), sonarUserHome.resolve("ssl/keystore.p12").toString()); - var keyStorePassword = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_KEYSTORE_PASSWORD), CertificateStore.DEFAULT_PASSWORD); - var trustStorePath = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_TRUSTSTORE_PATH), sonarUserHome.resolve("ssl/truststore.p12").toString()); - var trustStorePassword = defaultIfBlank(bootstrapProperties.get(SONAR_SCANNER_TRUSTSTORE_PASSWORD), CertificateStore.DEFAULT_PASSWORD); - var keyStore = new CertificateStore(Path.of(keyStorePath), keyStorePassword); - var trustStore = new CertificateStore(Path.of(trustStorePath), trustStorePassword); - return new SslConfig(keyStore, trustStore); - } - private static SSLFactory configureSsl(SslConfig sslConfig) { var sslFactoryBuilder = SSLFactory.builder() .withDefaultTrustMaterial() diff --git a/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ServerConnection.java b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClient.java similarity index 75% rename from lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ServerConnection.java rename to lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClient.java index 3d188915..611bb2a6 100644 --- a/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ServerConnection.java +++ b/lib/src/main/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClient.java @@ -24,7 +24,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.Map; import javax.annotation.Nullable; import okhttp3.Credentials; import okhttp3.OkHttpClient; @@ -33,49 +32,31 @@ import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sonarsource.scanner.lib.ScannerProperties; import org.sonarsource.scanner.lib.Utils; -import org.sonarsource.scanner.lib.internal.InternalProperties; import static java.lang.String.format; -public class ServerConnection { +public class ScannerHttpClient { - private static final Logger LOG = LoggerFactory.getLogger(ServerConnection.class); + private static final Logger LOG = LoggerFactory.getLogger(ScannerHttpClient.class); private static final String EXCEPTION_MESSAGE_MISSING_SLASH = "URL path must start with slash: %s"; - private String webApiBaseUrl; - private String restApiBaseUrl; - private String userAgent; - @Nullable - private String token; - @Nullable - private String login; - @Nullable - private String password; + private OkHttpClient httpClient; + private HttpConfig httpConfig; - public void init(Map bootstrapProperties, Path sonarUserHome) { - webApiBaseUrl = removeTrailingSlash(bootstrapProperties.get(ScannerProperties.HOST_URL)); - restApiBaseUrl = removeTrailingSlash(bootstrapProperties.get(ScannerProperties.API_BASE_URL)); - userAgent = format("%s/%s", bootstrapProperties.get(InternalProperties.SCANNER_APP), - bootstrapProperties.get(InternalProperties.SCANNER_APP_VERSION)); - this.token = bootstrapProperties.get(ScannerProperties.SONAR_TOKEN); - this.login = bootstrapProperties.get(ScannerProperties.SONAR_LOGIN); - this.password = bootstrapProperties.get(ScannerProperties.SONAR_PASSWORD); - httpClient = OkHttpClientFactory.create(bootstrapProperties, sonarUserHome); + public void init(HttpConfig httpConfig) { + this.httpConfig = httpConfig; + this.httpClient = OkHttpClientFactory.create(httpConfig); } - public static String removeTrailingSlash(String url) { - return url.replaceAll("(/)+$", ""); - } public void downloadFromRestApi(String urlPath, Path toFile) throws IOException { if (!urlPath.startsWith("/")) { throw new IllegalArgumentException(format(EXCEPTION_MESSAGE_MISSING_SLASH, urlPath)); } - String url = restApiBaseUrl + urlPath; + String url = httpConfig.getRestApiBaseUrl() + urlPath; downloadFile(url, toFile, true); } @@ -83,7 +64,7 @@ public void downloadFromWebApi(String urlPath, Path toFile) throws IOException { if (!urlPath.startsWith("/")) { throw new IllegalArgumentException(format(EXCEPTION_MESSAGE_MISSING_SLASH, urlPath)); } - String url = webApiBaseUrl + urlPath; + String url = httpConfig.getWebApiBaseUrl() + urlPath; downloadFile(url, toFile, true); } @@ -107,7 +88,7 @@ private void downloadFile(String url, Path toFile, boolean authentication) throw LOG.debug("Download {} to {}", url, toFile.toAbsolutePath()); try (ResponseBody responseBody = callUrl(url, authentication, "application/octet-stream"); - InputStream in = responseBody.byteStream()) { + InputStream in = responseBody.byteStream()) { Files.copy(in, toFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException | RuntimeException e) { Utils.deleteQuietly(toFile); @@ -119,7 +100,7 @@ public String callRestApi(String urlPath) throws IOException { if (!urlPath.startsWith("/")) { throw new IllegalArgumentException(format(EXCEPTION_MESSAGE_MISSING_SLASH, urlPath)); } - String url = restApiBaseUrl + urlPath; + String url = httpConfig.getRestApiBaseUrl() + urlPath; return callApi(url); } @@ -127,7 +108,7 @@ public String callWebApi(String urlPath) throws IOException { if (!urlPath.startsWith("/")) { throw new IllegalArgumentException(format(EXCEPTION_MESSAGE_MISSING_SLASH, urlPath)); } - String url = webApiBaseUrl + urlPath; + String url = httpConfig.getWebApiBaseUrl() + urlPath; return callApi(url); } @@ -159,12 +140,12 @@ private ResponseBody callUrl(String url, boolean authentication, @Nullable Strin var requestBuilder = new Request.Builder() .get() .url(url) - .addHeader("User-Agent", userAgent); + .addHeader("User-Agent", httpConfig.getUserAgent()); if (authentication) { - if (token != null) { - requestBuilder.header("Authorization", "Bearer " + token); - } else if (login != null) { - requestBuilder.header("Authorization", Credentials.basic(login, password != null ? password : "")); + if (httpConfig.getToken() != null) { + requestBuilder.header("Authorization", "Bearer " + httpConfig.getToken()); + } else if (httpConfig.getLogin() != null) { + requestBuilder.header("Authorization", Credentials.basic(httpConfig.getLogin(), httpConfig.getPassword() != null ? httpConfig.getPassword() : "")); } } if (acceptHeader != null) { diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapperTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapperTest.java index e06cfc2c..44506efc 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapperTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapperTest.java @@ -20,21 +20,29 @@ package org.sonarsource.scanner.lib; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junitpioneer.jupiter.RestoreSystemProperties; import org.mockito.Mockito; import org.sonarsource.scanner.lib.internal.InternalProperties; import org.sonarsource.scanner.lib.internal.IsolatedLauncherFactory; import org.sonarsource.scanner.lib.internal.ScannerEngineLauncher; import org.sonarsource.scanner.lib.internal.ScannerEngineLauncherFactory; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.HttpConfig; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; +import org.sonarsource.scanner.lib.internal.http.ssl.CertificateStore; +import org.sonarsource.scanner.lib.internal.http.ssl.SslConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -50,7 +58,7 @@ class ScannerEngineBootstrapperTest { - private final ServerConnection serverConnection = mock(ServerConnection.class); + private final ScannerHttpClient scannerHttpClient = mock(ScannerHttpClient.class); private final ScannerEngineLauncherFactory scannerEngineLauncherFactory = mock(ScannerEngineLauncherFactory.class); private final System2 system = mock(System2.class); @@ -67,17 +75,17 @@ public void setUp() { when(system.getProperty("os.arch")).thenReturn("x64"); var launcher = mock(ScannerEngineLauncher.class); - when(scannerEngineLauncherFactory.createLauncher(any(ServerConnection.class), any(FileCache.class), anyMap())) + when(scannerEngineLauncherFactory.createLauncher(any(ScannerHttpClient.class), any(FileCache.class), anyMap())) .thenReturn(launcher); - underTest = new ScannerEngineBootstrapper("Gradle", "3.1", system, serverConnection, + underTest = new ScannerEngineBootstrapper("Gradle", "3.1", system, scannerHttpClient, new IsolatedLauncherFactory(), scannerEngineLauncherFactory); } @Test void should_use_new_bootstrapping_with_default_url() throws Exception { try (var scannerEngineFacade = underTest.bootstrap()) { - verify(scannerEngineLauncherFactory).createLauncher(eq(serverConnection), any(FileCache.class), anyMap()); + verify(scannerEngineLauncherFactory).createLauncher(eq(scannerHttpClient), any(FileCache.class), anyMap()); assertThat(scannerEngineFacade.isSonarCloud()).isTrue(); } } @@ -85,16 +93,16 @@ void should_use_new_bootstrapping_with_default_url() throws Exception { @Test void should_use_new_bootstrapping_with_sonarcloud_url() throws Exception { try (var scannerEngineFacade = underTest.setBootstrapProperty(ScannerProperties.HOST_URL, "https://sonarcloud.io").bootstrap()) { - verify(scannerEngineLauncherFactory).createLauncher(eq(serverConnection), any(FileCache.class), anyMap()); + verify(scannerEngineLauncherFactory).createLauncher(eq(scannerHttpClient), any(FileCache.class), anyMap()); assertThat(scannerEngineFacade.isSonarCloud()).isTrue(); } } @Test void should_use_new_bootstrapping_with_sonarqube_10_6() throws Exception { - when(serverConnection.callRestApi("/analysis/version")).thenReturn(SQ_VERSION_NEW_BOOTSTRAPPING); + when(scannerHttpClient.callRestApi("/analysis/version")).thenReturn(SQ_VERSION_NEW_BOOTSTRAPPING); try (var scannerEngineFacade = underTest.setBootstrapProperty(ScannerProperties.HOST_URL, "http://localhost").bootstrap()) { - verify(scannerEngineLauncherFactory).createLauncher(eq(serverConnection), any(FileCache.class), anyMap()); + verify(scannerEngineLauncherFactory).createLauncher(eq(scannerHttpClient), any(FileCache.class), anyMap()); assertThat(scannerEngineFacade.isSonarCloud()).isFalse(); assertThat(scannerEngineFacade.getServerVersion()).isEqualTo(SQ_VERSION_NEW_BOOTSTRAPPING); } @@ -103,16 +111,16 @@ void should_use_new_bootstrapping_with_sonarqube_10_6() throws Exception { @Test void should_use_old_bootstrapping_with_sonarqube_10_5() throws Exception { IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class); - when(launcherFactory.createLauncher(eq(serverConnection), any(FileCache.class))) + when(launcherFactory.createLauncher(eq(scannerHttpClient), any(FileCache.class))) .thenReturn(mock(IsolatedLauncherFactory.IsolatedLauncherAndClassloader.class)); - ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, serverConnection, + ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, scannerHttpClient, launcherFactory, scannerEngineLauncherFactory); - when(serverConnection.callRestApi("/analysis/version")).thenThrow(new IOException("404 Not found")); - when(serverConnection.callWebApi("/api/server/version")).thenReturn("10.5"); + when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new IOException("404 Not found")); + when(scannerHttpClient.callWebApi("/api/server/version")).thenReturn("10.5"); try (var scannerEngineFacade = bootstrapper.setBootstrapProperty(ScannerProperties.HOST_URL, "http://localhost").bootstrap()) { - verify(launcherFactory).createLauncher(eq(serverConnection), any(FileCache.class)); + verify(launcherFactory).createLauncher(eq(scannerHttpClient), any(FileCache.class)); assertThat(scannerEngineFacade.isSonarCloud()).isFalse(); assertThat(scannerEngineFacade.getServerVersion()).isEqualTo("10.5"); } @@ -121,13 +129,13 @@ void should_use_old_bootstrapping_with_sonarqube_10_5() throws Exception { @Test void should_preserve_both_exceptions_when_checking_version() throws Exception { IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class); - when(launcherFactory.createLauncher(eq(serverConnection), any(FileCache.class))) + when(launcherFactory.createLauncher(eq(scannerHttpClient), any(FileCache.class))) .thenReturn(mock(IsolatedLauncherFactory.IsolatedLauncherAndClassloader.class)); - ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, serverConnection, + ScannerEngineBootstrapper bootstrapper = new ScannerEngineBootstrapper("Gradle", "3.1", system, scannerHttpClient, launcherFactory, scannerEngineLauncherFactory); - when(serverConnection.callRestApi("/analysis/version")).thenThrow(new IOException("404 Not found")); - when(serverConnection.callWebApi("/api/server/version")).thenThrow(new IOException("400 Server Error")); + when(scannerHttpClient.callRestApi("/analysis/version")).thenThrow(new IOException("404 Not found")); + when(scannerHttpClient.callWebApi("/api/server/version")).thenThrow(new IOException("400 Server Error")); assertThatThrownBy(() -> { try (var ignored = bootstrapper.setBootstrapProperty(ScannerProperties.HOST_URL, "http://localhost").bootstrap()) { @@ -253,4 +261,53 @@ void should_not_override_os_and_arch_when_passed() throws Exception { assertThat(scannerEngine.getBootstrapProperties()).containsEntry("sonar.scanner.arch", "some-arch"); } } + + @Test + void should_set_deprecated_timeout_property() { + var httpConfig = mock(HttpConfig.class); + when(httpConfig.getSslConfig()).thenReturn(new SslConfig(null, null)); + + when(httpConfig.getSocketTimeout()).thenReturn(Duration.ofSeconds(100)); + + var adapted = underTest.adaptDeprecatedProperties(Map.of(), httpConfig); + assertThat(adapted).containsEntry("sonar.ws.timeout", "100"); + } + + @Test + @RestoreSystemProperties + void should_set_deprecated_proxy_properties() { + var httpConfig = mock(HttpConfig.class); + when(httpConfig.getSslConfig()).thenReturn(new SslConfig(null, null)); + when(httpConfig.getSocketTimeout()).thenReturn(Duration.ofSeconds(10)); + + when(httpConfig.getProxy()).thenReturn(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("myproxy", 8080))); + + underTest.adaptDeprecatedProperties(Map.of(), httpConfig); + + assertThat(System.getProperties()).contains( + entry("http.proxyHost", "myproxy"), + entry("https.proxyHost", "myproxy"), + entry("http.proxyPort", "8080"), + entry("https.proxyPort", "8080")); + } + + @Test + @RestoreSystemProperties + void should_set_deprecated_ssl_properties() { + var httpConfig = mock(HttpConfig.class); + when(httpConfig.getSocketTimeout()).thenReturn(Duration.ofSeconds(10)); + + when(httpConfig.getSslConfig()) + .thenReturn(new SslConfig( + new CertificateStore(Paths.get("some/keystore.p12"), "keystorePass"), + new CertificateStore(Paths.get("some/truststore.p12"), "truststorePass"))); + + underTest.adaptDeprecatedProperties(Map.of(), httpConfig); + + assertThat(System.getProperties()).contains( + entry("javax.net.ssl.keyStore", "some/keystore.p12"), + entry("javax.net.ssl.keyStorePassword", "keystorePass"), + entry("javax.net.ssl.trustStore", "some/truststore.p12"), + entry("javax.net.ssl.trustStorePassword", "truststorePass")); + } } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloaderTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloaderTest.java index bb03cbc6..6255faa6 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloaderTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/BootstrapIndexDownloaderTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.scanner.lib.internal.BootstrapIndexDownloader.JarEntry; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -35,12 +35,12 @@ import static org.mockito.Mockito.when; class BootstrapIndexDownloaderTest { - private ServerConnection connection; + private ScannerHttpClient connection; private BootstrapIndexDownloader bootstrapIndexDownloader; @BeforeEach void setUp() { - connection = mock(ServerConnection.class); + connection = mock(ScannerHttpClient.class); bootstrapIndexDownloader = new BootstrapIndexDownloader(connection); } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactoryTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactoryTest.java index 853b3a0f..27986e2d 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactoryTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/JavaRunnerFactoryTest.java @@ -35,7 +35,7 @@ import org.sonarsource.scanner.lib.System2; import org.sonarsource.scanner.lib.internal.cache.CachedFile; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; @@ -53,7 +53,7 @@ class JavaRunnerFactoryTest { - private final ServerConnection serverConnection = mock(ServerConnection.class); + private final ScannerHttpClient scannerHttpClient = mock(ScannerHttpClient.class); private final FileCache fileCache = mock(FileCache.class); private final System2 system = mock(System2.class); private final ProcessWrapperFactory processWrapperFactory = mock(ProcessWrapperFactory.class); @@ -73,11 +73,11 @@ void createRunner_jreProvisioning() throws IOException { var jre = temp.resolve("fake-jre.zip"); FileUtils.copyFile(new File("src/test/resources/fake-jre.zip"), jre.toFile()); - when(serverConnection.callRestApi(matches(API_PATH_JRE + ".*"))).thenReturn( + when(scannerHttpClient.callRestApi(matches(API_PATH_JRE + ".*"))).thenReturn( IOUtils.toString(requireNonNull(getClass().getResourceAsStream("createRunner_jreProvisioning.json")), StandardCharsets.UTF_8)); when(fileCache.getOrDownload(eq("fake-jre.zip"), eq("123456"), eq("SHA-256"), any(JavaRunnerFactory.JreDownloader.class))).thenReturn(new CachedFile(jre, true)); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, new HashMap<>()); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, new HashMap<>()); assertThat(runner.getJavaExecutable()).isNotNull(); assertThat(runner.getJavaExecutable()).exists(); @@ -85,10 +85,10 @@ void createRunner_jreProvisioning() throws IOException { @Test void createRunner_jreProvisioning_noMatch_fallback_to_local() throws IOException { - when(serverConnection.callRestApi(matches(API_PATH_JRE + ".*"))).thenReturn("[]"); + when(scannerHttpClient.callRestApi(matches(API_PATH_JRE + ".*"))).thenReturn("[]"); Map props = Map.of(SCANNER_OS, "linux", SCANNER_ARCH, "x64"); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, props); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, props); assertThat(runner.getJavaExecutable()).isEqualTo(Paths.get("java")); assertThat(runner.getJreCacheHit()).isEqualTo(JreCacheHit.DISABLED); @@ -99,7 +99,7 @@ void createRunner_jreExeProperty() { var javaExe = temp.resolve("bin/java"); Map properties = Map.of(JAVA_EXECUTABLE_PATH, javaExe.toAbsolutePath().toString()); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, properties); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, properties); assertThat(runner.getJavaExecutable()).isEqualTo(javaExe); assertThat(runner.getJreCacheHit()).isEqualTo(JreCacheHit.DISABLED); @@ -114,7 +114,7 @@ void createRunner_jreProvisioningSkipAndJavaHome() throws IOException { when(system.getEnvironmentVariable("JAVA_HOME")).thenReturn(javaHome.toString()); Map properties = Map.of(SKIP_JRE_PROVISIONING, "true"); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, properties); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, properties); assertThat(runner.getJavaExecutable()).isEqualTo(temp.resolve("bin/java")); assertThat(runner.getJreCacheHit()).isEqualTo(JreCacheHit.DISABLED); @@ -124,7 +124,7 @@ void createRunner_jreProvisioningSkipAndJavaHome() throws IOException { void createRunner_jreProvisioningSkipAndNoJavaHome() { Map properties = Map.of(SKIP_JRE_PROVISIONING, "true"); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, properties); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, properties); assertThat(runner.getJavaExecutable()).isEqualTo(Paths.get("java")); assertThat(runner.getJreCacheHit()).isEqualTo(JreCacheHit.DISABLED); @@ -140,7 +140,7 @@ void createRunner_jreProvisioningSkipAndNoJavaHome_windows() throws IOException, Map properties = Map.of(SKIP_JRE_PROVISIONING, "true"); - JavaRunner runner = underTest.createRunner(serverConnection, fileCache, properties); + JavaRunner runner = underTest.createRunner(scannerHttpClient, fileCache, properties); assertThat(runner.getJavaExecutable()).isEqualTo(Paths.get("C:\\bin\\java.exe")); assertThat(runner.getJreCacheHit()).isEqualTo(JreCacheHit.DISABLED); @@ -150,19 +150,19 @@ void createRunner_jreProvisioningSkipAndNoJavaHome_windows() throws IOException, void jreDownloader_download() throws IOException { String filename = "jre.zip"; var output = temp.resolve(filename); - new JavaRunnerFactory.JreDownloader(serverConnection, + new JavaRunnerFactory.JreDownloader(scannerHttpClient, new JavaRunnerFactory.JreMetadata(filename, "123456", null, "uuid", "bin/java")) .download(filename, output); - verify(serverConnection).downloadFromRestApi(API_PATH_JRE + "/uuid", output); + verify(scannerHttpClient).downloadFromRestApi(API_PATH_JRE + "/uuid", output); } @Test void jreDownloader_download_withDownloadUrl() throws IOException { String filename = "jre.zip"; var output = temp.resolve(filename); - new JavaRunnerFactory.JreDownloader(serverConnection, + new JavaRunnerFactory.JreDownloader(scannerHttpClient, new JavaRunnerFactory.JreMetadata(filename, "123456", "https://localhost/jre.zip", "uuid", "bin/java")) .download(filename, output); - verify(serverConnection).downloadFromExternalUrl("https://localhost/jre.zip", output); + verify(scannerHttpClient).downloadFromExternalUrl("https://localhost/jre.zip", output); } } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactoryTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactoryTest.java index 75a9c4ab..efa77156 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactoryTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderFactoryTest.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -29,7 +29,7 @@ class LegacyScannerEngineDownloaderFactoryTest { @Test void should_create() { - ServerConnection conn = mock(ServerConnection.class); + ScannerHttpClient conn = mock(ScannerHttpClient.class); FileCache cache = mock(FileCache.class); assertThat(new LegacyScannerEngineDownloaderFactory(conn, cache).create()).isNotNull(); } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderTest.java index e469039e..32a2b9aa 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/LegacyScannerEngineDownloaderTest.java @@ -28,7 +28,7 @@ import org.sonarsource.scanner.lib.internal.BootstrapIndexDownloader.JarEntry; import org.sonarsource.scanner.lib.internal.LegacyScannerEngineDownloader.ScannerFileDownloader; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -69,7 +69,7 @@ void should_download_jar_files(@TempDir Path tmpDir) { @Test void test_jar_downloader(@TempDir Path tmpDir) throws Exception { - ServerConnection connection = mock(ServerConnection.class); + ScannerHttpClient connection = mock(ScannerHttpClient.class); LegacyScannerEngineDownloader.ScannerFileDownloader downloader = new LegacyScannerEngineDownloader.ScannerFileDownloader(connection); var toFile = Files.createTempFile(tmpDir, "squid", ".jar"); downloader.download("squid.jar", toFile); diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactoryTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactoryTest.java index 9a4b16f3..a98362a9 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactoryTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/ScannerEngineLauncherFactoryTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.scanner.lib.internal.cache.FileCache; -import org.sonarsource.scanner.lib.internal.http.ServerConnection; +import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; @@ -37,7 +37,7 @@ class ScannerEngineLauncherFactoryTest { - private final ServerConnection serverConnection = mock(ServerConnection.class); + private final ScannerHttpClient scannerHttpClient = mock(ScannerHttpClient.class); private final FileCache fileCache = mock(FileCache.class); private final JavaRunnerFactory javaRunnerFactory = mock(JavaRunnerFactory.class); @@ -46,11 +46,11 @@ class ScannerEngineLauncherFactoryTest { @Test void createLauncher() throws IOException { - when(serverConnection.callRestApi(API_PATH_ENGINE)).thenReturn("{\"filename\":\"scanner-engine.jar\",\"sha256\":\"123456\"}"); - when(javaRunnerFactory.createRunner(eq(serverConnection), eq(fileCache), anyMap())).thenReturn(mock(JavaRunner.class)); + when(scannerHttpClient.callRestApi(API_PATH_ENGINE)).thenReturn("{\"filename\":\"scanner-engine.jar\",\"sha256\":\"123456\"}"); + when(javaRunnerFactory.createRunner(eq(scannerHttpClient), eq(fileCache), anyMap())).thenReturn(mock(JavaRunner.class)); ScannerEngineLauncherFactory factory = new ScannerEngineLauncherFactory(javaRunnerFactory); - factory.createLauncher(serverConnection, fileCache, new HashMap<>()); + factory.createLauncher(scannerHttpClient, fileCache, new HashMap<>()); verify(fileCache).getOrDownload(eq("scanner-engine.jar"), eq("123456"), eq("SHA-256"), any(ScannerEngineLauncherFactory.ScannerEngineDownloader.class)); @@ -60,19 +60,19 @@ void createLauncher() throws IOException { void scannerEngineDownloader_download() throws IOException { String filename = "scanner-engine.jar"; var output = temp.resolve(filename); - new ScannerEngineLauncherFactory.ScannerEngineDownloader(serverConnection, + new ScannerEngineLauncherFactory.ScannerEngineDownloader(scannerHttpClient, new ScannerEngineLauncherFactory.ScannerEngineMetadata(filename, "123456", null)) .download(filename, output); - verify(serverConnection).downloadFromRestApi(API_PATH_ENGINE, output); + verify(scannerHttpClient).downloadFromRestApi(API_PATH_ENGINE, output); } @Test void scannerEngineDownloader_download_withDownloadUrl() throws IOException { String filename = "scanner-engine.jar"; var output = temp.resolve(filename); - new ScannerEngineLauncherFactory.ScannerEngineDownloader(serverConnection, + new ScannerEngineLauncherFactory.ScannerEngineDownloader(scannerHttpClient, new ScannerEngineLauncherFactory.ScannerEngineMetadata(filename, "123456", "https://localhost/scanner-engine.jar")) .download(filename, output); - verify(serverConnection).downloadFromExternalUrl("https://localhost/scanner-engine.jar", output); + verify(scannerHttpClient).downloadFromExternalUrl("https://localhost/scanner-engine.jar", output); } } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/HttpConfigTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/HttpConfigTest.java new file mode 100644 index 00000000..1b3b1205 --- /dev/null +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/HttpConfigTest.java @@ -0,0 +1,78 @@ +/* + * SonarScanner Java Library + * Copyright (C) 2011-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.sonarsource.scanner.lib.internal.http; + +import java.nio.file.Path; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class HttpConfigTest { + + private static final String SONAR_WS_TIMEOUT = "sonar.ws.timeout"; + + private final Map bootstrapProperties = new HashMap<>(); + + @TempDir + private Path sonarUserHomeDir; + private Path sonarUserHome; + + @BeforeEach + void prepareMocks() { + this.sonarUserHome = sonarUserHomeDir; + bootstrapProperties.clear(); + } + + @Test + void support_custom_timeouts() { + int readTimeoutSec = 2000; + + HttpConfig underTest = new HttpConfig(Map.of(SONAR_WS_TIMEOUT, String.valueOf(readTimeoutSec)), sonarUserHome); + + assertThat(underTest.getSocketTimeout()).isEqualTo(Duration.of(2000, ChronoUnit.SECONDS)); + } + + @Test + void support_custom_timeouts_throws_exception_on_non_number() { + var props = Map.of(SONAR_WS_TIMEOUT, "fail"); + assertThatThrownBy(() -> new HttpConfig(props, sonarUserHome)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(SONAR_WS_TIMEOUT + " is not a valid integer: fail"); + } + + @Test + void it_should_throw_if_invalid_proxy_port() { + bootstrapProperties.put("sonar.scanner.proxyHost", "localhost"); + bootstrapProperties.put("sonar.scanner.proxyPort", "not_a_number"); + + assertThatThrownBy(() -> new HttpConfig(bootstrapProperties, sonarUserHome)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("sonar.scanner.proxyPort is not a valid integer: not_a_number"); + } + + +} diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactoryTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactoryTest.java index ecf9130a..ebf7e2e8 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactoryTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/OkHttpClientFactoryTest.java @@ -30,7 +30,6 @@ import java.util.HashMap; import java.util.Map; import javax.net.ssl.SSLHandshakeException; -import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.junit.jupiter.api.BeforeEach; @@ -60,7 +59,6 @@ class OkHttpClientFactoryTest { - private static final String SONAR_WS_TIMEOUT = "sonar.ws.timeout"; private static final String COOKIE = "BIGipServerpool_sonarqube.example.com_8443=123456789.12345.0000"; private final Map bootstrapProperties = new HashMap<>(); @@ -75,23 +73,6 @@ void prepareMocks() { bootstrapProperties.clear(); } - @Test - void support_custom_timeouts() { - int readTimeoutSec = 2000; - - OkHttpClient underTest = OkHttpClientFactory.create(Map.of(SONAR_WS_TIMEOUT, String.valueOf(readTimeoutSec)), sonarUserHome); - - assertThat(underTest.readTimeoutMillis()).isEqualTo(readTimeoutSec * 1000); - } - - @Test - void support_custom_timeouts_throws_exception_on_non_number() { - var props = Map.of(SONAR_WS_TIMEOUT, "fail"); - assertThatThrownBy(() -> OkHttpClientFactory.create(props, sonarUserHome)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("sonar.scanner.socketTimeout is not a valid integer: fail"); - } - @Nested // Workaround until we move to Java 17+ and can make Wiremock extension static @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -174,17 +155,6 @@ void it_should_timeout_on_slow_response() { .hasStackTraceContaining("timeout"); } - @Test - void it_should_throw_if_invalid_proxy_port() { - bootstrapProperties.put("sonar.host.url", sonarqubeMock.baseUrl()); - bootstrapProperties.put("sonar.scanner.proxyHost", "localhost"); - bootstrapProperties.put("sonar.scanner.proxyPort", "not_a_number"); - - assertThatThrownBy(() -> OkHttpClientFactory.create(bootstrapProperties, sonarUserHome)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("sonar.scanner.proxyPort is not a valid integer: not_a_number"); - } - @Nested // Workaround until we move to Java 17+ and can make Wiremock extension static @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -395,11 +365,11 @@ void it_should_support_jvm_system_properties() throws IOException { } private Response call(String url) throws IOException { - return OkHttpClientFactory.create(bootstrapProperties, sonarUserHome).newCall( - new Request.Builder() - .url(url) - .get() - .build()) + return OkHttpClientFactory.create(new HttpConfig(bootstrapProperties, sonarUserHome)).newCall( + new Request.Builder() + .url(url) + .get() + .build()) .execute(); } diff --git a/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ServerConnectionTest.java b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClientTest.java similarity index 87% rename from lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ServerConnectionTest.java rename to lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClientTest.java index 51cb1aaf..2c792b80 100644 --- a/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ServerConnectionTest.java +++ b/lib/src/test/java/org/sonarsource/scanner/lib/internal/http/ScannerHttpClientTest.java @@ -42,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class ServerConnectionTest { +class ScannerHttpClientTest { private static final String HELLO_WORLD = "hello, world!"; @@ -56,7 +56,7 @@ class ServerConnectionTest { @Test void download_success() throws Exception { - ServerConnection connection = create(); + ScannerHttpClient connection = create(); answer(HELLO_WORLD); String response = connection.callWebApi("/batch/index.txt"); @@ -66,7 +66,7 @@ void download_success() throws Exception { @Test void callWebApi_fails_on_url_validation() { - ServerConnection connection = create(); + ScannerHttpClient connection = create(); answer(HELLO_WORLD); assertThatThrownBy(() -> connection.callWebApi("should_fail")) @@ -79,7 +79,7 @@ void test_downloadFromWebApi(@TempDir Path tmpFolder) throws Exception { var toFile = tmpFolder.resolve("index.txt"); answer(HELLO_WORLD); - ServerConnection underTest = create(); + ScannerHttpClient underTest = create(); underTest.downloadFromWebApi("/batch/index.txt", toFile); assertThat(Files.readString(toFile)).isEqualTo(HELLO_WORLD); @@ -88,7 +88,7 @@ void test_downloadFromWebApi(@TempDir Path tmpFolder) throws Exception { @Test void downloadFromWebApi_fails_on_url_validation(@TempDir Path tmpFolder) { var toFile = tmpFolder.resolve("index.txt"); - ServerConnection connection = create(); + ScannerHttpClient connection = create(); answer(HELLO_WORLD); assertThatThrownBy(() -> connection.downloadFromWebApi("should_fail", toFile)) @@ -101,7 +101,7 @@ void should_throw_ISE_if_response_not_successful(@TempDir Path tmpFolder) { var toFile = tmpFolder.resolve("index.txt"); answer(HELLO_WORLD, 400); - ServerConnection underTest = create(); + ScannerHttpClient underTest = create(); assertThatThrownBy(() -> underTest.downloadFromWebApi("/batch/index.txt", toFile)) .isInstanceOf(IllegalStateException.class) .hasMessage(format("Error status returned by url [http://%s:%d/batch/index.txt]: 400", "localhost", sonarqube.getPort())); @@ -109,7 +109,7 @@ void should_throw_ISE_if_response_not_successful(@TempDir Path tmpFolder) { @Test void should_support_server_url_without_trailing_slash() throws Exception { - ServerConnection connection = create(sonarqube.baseUrl().replaceAll("(/)+$", "")); + ScannerHttpClient connection = create(sonarqube.baseUrl().replaceAll("(/)+$", "")); answer(HELLO_WORLD); String content = connection.callWebApi("/batch/index.txt"); @@ -118,7 +118,7 @@ void should_support_server_url_without_trailing_slash() throws Exception { @Test void should_support_server_url_with_trailing_slash() throws Exception { - ServerConnection connection = create(sonarqube.baseUrl().replaceAll("(/)+$", "") + "/"); + ScannerHttpClient connection = create(sonarqube.baseUrl().replaceAll("(/)+$", "") + "/"); answer(HELLO_WORLD); String content = connection.callWebApi("/batch/index.txt"); @@ -129,7 +129,7 @@ void should_support_server_url_with_trailing_slash() throws Exception { void should_authenticate_with_token() throws Exception { Map props = new HashMap<>(); props.put("sonar.token", "some_token"); - ServerConnection connection = create(sonarqube.baseUrl(), props); + ScannerHttpClient connection = create(sonarqube.baseUrl(), props); answer(HELLO_WORLD); String content = connection.callWebApi("/batch/index.txt"); @@ -144,7 +144,7 @@ void should_authenticate_with_username_password() throws Exception { Map props = new HashMap<>(); props.put("sonar.login", "some_username"); props.put("sonar.password", "some_password"); - ServerConnection connection = create(sonarqube.baseUrl(), props); + ScannerHttpClient connection = create(sonarqube.baseUrl(), props); answer(HELLO_WORLD); String content = connection.callWebApi("/batch/index.txt"); @@ -160,7 +160,7 @@ void downloadFromExternalUrl_shouldNotPassAuth(@TempDir Path tmpFolder) throws E var toFile = tmpFolder.resolve("index.txt"); answer(HELLO_WORLD); - ServerConnection underTest = create(); + ScannerHttpClient underTest = create(); underTest.downloadFromExternalUrl(sonarqube.baseUrl() + "/batch/index.txt", toFile); assertThat(Files.readString(toFile)).isEqualTo(HELLO_WORLD); @@ -168,15 +168,15 @@ void downloadFromExternalUrl_shouldNotPassAuth(@TempDir Path tmpFolder) throws E .withoutHeader("Authorization")); } - private ServerConnection create() { + private ScannerHttpClient create() { return create(sonarqube.baseUrl()); } - private ServerConnection create(String url) { + private ScannerHttpClient create(String url) { return create(url, new HashMap<>()); } - private ServerConnection create(String url, Map additionalProps) { + private ScannerHttpClient create(String url, Map additionalProps) { Map props = new HashMap<>(); props.put(ScannerProperties.HOST_URL, url); props.put(ScannerProperties.API_BASE_URL, url); @@ -184,8 +184,8 @@ private ServerConnection create(String url, Map additionalProps) props.put(InternalProperties.SCANNER_APP_VERSION, "agent"); props.putAll(additionalProps); - ServerConnection connection = new ServerConnection(); - connection.init(props, sonarUserHome); + ScannerHttpClient connection = new ScannerHttpClient(); + connection.init(new HttpConfig(props, sonarUserHome)); return connection; }