aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulien HENRY <julien.henry@sonarsource.com>2024-04-23 09:22:26 +0200
committerMatteo Mara <matteo.mara@sonarsource.com>2024-04-30 10:58:03 +0200
commitce849ff5f214ccda873a9e43a8aa4827d37caf92 (patch)
tree885186c6cb15e8733236042dee36e0d153875bf0
parentd42e73cd8ae1c41f60c828375561e9767d941570 (diff)
downloadsonarqube-ce849ff5f214ccda873a9e43a8aa4827d37caf92.tar.gz
sonarqube-ce849ff5f214ccda873a9e43a8aa4827d37caf92.zip
SONAR-22038 Support new SSL properties (#10987)
* Move scanner HttpClient code in its own package * Factorize the computation of the Sonar User Home
-rw-r--r--build.gradle2
-rw-r--r--sonar-scanner-engine/build.gradle2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java44
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java108
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java1
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java35
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java66
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java (renamed from sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java)3
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java (renamed from sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java)2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java (renamed from sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java)46
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java48
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java43
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java23
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java2
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java2
-rw-r--r--sonar-scanner-engine/src/main/resources/logback.xml5
-rw-r--r--sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml3
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java128
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java172
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java1
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java251
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java67
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java (renamed from sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java)4
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java413
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java2
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/README.md452
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt33
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key52
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/ca.crt33
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/ca.key52
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12bin0 -> 1846 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/client.csr29
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/client.key52
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/client.p12bin0 -> 4330 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/client.pem31
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf33
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/openssl.conf34
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12bin0 -> 1862 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/server.csr29
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/server.key52
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/server.p12bin0 -> 4438 bytes
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/server.pem33
-rw-r--r--sonar-scanner-engine/src/test/resources/ssl/v3.ext7
69 files changed, 1935 insertions, 533 deletions
diff --git a/build.gradle b/build.gradle
index a4a32bbb0ea..408182a06e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -315,6 +315,7 @@ subprojects {
exclude 'junit:junit'
}
dependency 'com.squareup.okio:okio:3.7.0'
+ dependency 'io.github.hakky54:sslcontext-kickstart:8.3.4'
dependency 'io.prometheus:simpleclient:0.16.0'
dependency 'io.prometheus:simpleclient_common:0.16.0'
dependency 'io.prometheus:simpleclient_servlet:0.16.0'
@@ -370,6 +371,7 @@ subprojects {
entry 'junit-jupiter-params'
entry 'junit-vintage-engine'
}
+ dependency 'org.junit-pioneer:junit-pioneer:2.2.0'
dependency 'org.xmlunit:xmlunit-core:2.9.1'
dependency 'org.xmlunit:xmlunit-matchers:2.9.1'
dependency 'org.lz4:lz4-java:1.8.0'
diff --git a/sonar-scanner-engine/build.gradle b/sonar-scanner-engine/build.gradle
index af2349194a5..e6a289228b8 100644
--- a/sonar-scanner-engine/build.gradle
+++ b/sonar-scanner-engine/build.gradle
@@ -27,6 +27,7 @@ dependencies {
api 'com.google.protobuf:protobuf-java'
api 'com.squareup.okhttp3:okhttp'
api 'com.fasterxml.staxmate:staxmate'
+ implementation 'io.github.hakky54:sslcontext-kickstart'
api 'javax.annotation:javax.annotation-api'
api 'org.eclipse.jgit:org.eclipse.jgit'
api 'org.tmatesoft.svnkit:svnkit'
@@ -64,6 +65,7 @@ dependencies {
testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
testImplementation project(':plugins:sonar-xoo-plugin')
testImplementation 'org.wiremock:wiremock-standalone'
+ testImplementation 'org.junit-pioneer:junit-pioneer'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java
index 821529b3d60..07711d1b782 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.CoreProperties;
import org.sonar.api.impl.utils.DefaultTempFolder;
-import org.sonar.api.utils.System2;
import org.sonar.api.utils.TempFolder;
import org.springframework.context.annotation.Bean;
@@ -42,24 +41,13 @@ public class GlobalTempFolderProvider {
private static final long CLEAN_MAX_AGE = TimeUnit.DAYS.toMillis(21);
static final String TMP_NAME_PREFIX = ".sonartmp_";
- private System2 system;
-
- public GlobalTempFolderProvider() {
- this(new System2());
- }
-
- GlobalTempFolderProvider(System2 system) {
- this.system = system;
- }
-
@Bean("GlobalTempFolder")
- public TempFolder provide(ScannerProperties scannerProps) {
-
- String workingPathName = StringUtils.defaultIfBlank(scannerProps.property(CoreProperties.GLOBAL_WORKING_DIRECTORY), CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
- Path workingPath = Paths.get(workingPathName);
+ public TempFolder provide(ScannerProperties scannerProps, SonarUserHome userHome) {
+ var workingPathName = StringUtils.defaultIfBlank(scannerProps.property(CoreProperties.GLOBAL_WORKING_DIRECTORY), CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
+ var workingPath = Paths.get(workingPathName);
if (!workingPath.isAbsolute()) {
- Path home = findSonarHome(scannerProps);
+ var home = userHome.getPath();
workingPath = home.resolve(workingPath).normalize();
}
try {
@@ -67,7 +55,7 @@ public class GlobalTempFolderProvider {
} catch (IOException e) {
LOG.error(String.format("failed to clean global working directory: %s", workingPath), e);
}
- Path tempDir = createTempFolder(workingPath);
+ var tempDir = createTempFolder(workingPath);
return new DefaultTempFolder(tempDir.toFile(), true);
}
@@ -90,24 +78,8 @@ public class GlobalTempFolderProvider {
}
}
- private Path findSonarHome(ScannerProperties props) {
- String home = props.property("sonar.userHome");
- if (home != null) {
- return Paths.get(home).toAbsolutePath();
- }
-
- home = system.envVariable("SONAR_USER_HOME");
-
- if (home != null) {
- return Paths.get(home).toAbsolutePath();
- }
-
- home = system.property("user.home");
- return Paths.get(home, ".sonar").toAbsolutePath();
- }
-
private static void cleanTempFolders(Path path) throws IOException {
- if (path.toFile().exists()) {
+ if (Files.exists(path)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, new CleanFilter())) {
for (Path p : stream) {
deleteQuietly(p.toFile());
@@ -118,8 +90,8 @@ public class GlobalTempFolderProvider {
private static class CleanFilter implements DirectoryStream.Filter<Path> {
@Override
- public boolean accept(Path path) throws IOException {
- if (!path.toFile().exists()) {
+ public boolean accept(Path path) {
+ if (!Files.exists(path)) {
return false;
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java
index 727ada7fbb2..ba53811a9a8 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java
@@ -26,15 +26,16 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.file.Files;
-import java.util.Objects;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.Optional;
-import java.util.stream.Stream;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.config.Configuration;
import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.HttpException;
import org.sonarqube.ws.client.WsResponse;
@@ -51,21 +52,21 @@ public class PluginFiles {
private final DefaultScannerWsClient wsClient;
private final Configuration configuration;
- private final File cacheDir;
- private final File tempDir;
+ private final Path cacheDir;
+ private final Path tempDir;
- public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration) {
+ public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration, SonarUserHome sonarUserHome) {
this.wsClient = wsClient;
this.configuration = configuration;
- File home = locateHomeDir(configuration);
- this.cacheDir = mkdir(new File(home, "cache"), "user cache");
- this.tempDir = mkdir(new File(home, "_tmp"), "temp dir");
- LOGGER.info("User cache: {}", cacheDir.getAbsolutePath());
+ var home = sonarUserHome.getPath();
+ this.cacheDir = mkdir(home.resolve("cache"), "user cache");
+ this.tempDir = mkdir(home.resolve("_tmp"), "temp dir");
+ LOGGER.info("User cache: {}", cacheDir);
}
public File createTempDir() {
try {
- return Files.createTempDirectory(tempDir.toPath(), "plugins").toFile();
+ return Files.createTempDirectory(tempDir, "plugins").toFile();
} catch (IOException e) {
throw new IllegalStateException("Fail to create temp directory in " + tempDir, e);
}
@@ -81,24 +82,24 @@ public class PluginFiles {
*/
public Optional<File> get(InstalledPlugin plugin) {
// Does not fail if another process tries to create the directory at the same time.
- File jarInCache = jarInCache(plugin.key, plugin.hash);
- if (jarInCache.exists() && jarInCache.isFile()) {
- return Optional.of(jarInCache);
+ Path jarInCache = jarInCache(plugin.key, plugin.hash);
+ if (Files.isRegularFile(jarInCache)) {
+ return Optional.of(jarInCache.toFile());
}
- return download(plugin);
+ return download(plugin).map(Path::toFile);
}
- private Optional<File> download(InstalledPlugin plugin) {
+ private Optional<Path> download(InstalledPlugin plugin) {
GetRequest request = new GetRequest("api/plugins/download")
.setParam("plugin", plugin.key)
.setTimeOutInMs(configuration.getInt(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY).orElse(PLUGINS_DOWNLOAD_TIMEOUT_DEFAULT) * 1000);
- File downloadedFile = newTempFile();
+ Path downloadedFile = newTempFile();
LOGGER.debug("Download plugin '{}' to '{}'", plugin.key, downloadedFile);
try (WsResponse response = wsClient.call(request)) {
Optional<String> expectedMd5 = response.header(MD5_HEADER);
- if (!expectedMd5.isPresent()) {
+ if (expectedMd5.isEmpty()) {
throw new IllegalStateException(format(
"Fail to download plugin [%s]. Request to %s did not return header %s", plugin.key, response.requestUrl(), MD5_HEADER));
}
@@ -114,14 +115,14 @@ public class PluginFiles {
// un-compress if needed
String cacheMd5;
- File tempJar;
+ Path tempJar;
tempJar = downloadedFile;
cacheMd5 = expectedMd5.get();
// put in cache
- File jarInCache = jarInCache(plugin.key, cacheMd5);
- mkdir(jarInCache.getParentFile());
+ Path jarInCache = jarInCache(plugin.key, cacheMd5);
+ mkdir(jarInCache.getParent());
moveFile(tempJar, jarInCache);
return Optional.of(jarInCache);
@@ -137,82 +138,75 @@ public class PluginFiles {
}
}
- private static void downloadBinaryTo(InstalledPlugin plugin, File downloadedFile, WsResponse response) {
+ private static void downloadBinaryTo(InstalledPlugin plugin, Path downloadedFile, WsResponse response) {
try (InputStream stream = response.contentStream()) {
- FileUtils.copyInputStreamToFile(stream, downloadedFile);
+ FileUtils.copyInputStreamToFile(stream, downloadedFile.toFile());
} catch (IOException e) {
throw new IllegalStateException(format("Fail to download plugin [%s] into %s", plugin.key, downloadedFile), e);
}
}
- private File jarInCache(String pluginKey, String hash) {
- File hashDir = new File(cacheDir, hash);
- File file = new File(hashDir, format("sonar-%s-plugin.jar", pluginKey));
- if (!file.getParentFile().toPath().equals(hashDir.toPath())) {
+ private Path jarInCache(String pluginKey, String hash) {
+ Path hashDir = cacheDir.resolve(hash);
+ Path file = hashDir.resolve(format("sonar-%s-plugin.jar", pluginKey));
+ if (!file.getParent().equals(hashDir)) {
// vulnerability - attempt to create a file outside the cache directory
throw new IllegalStateException(format("Fail to download plugin [%s]. Key is not valid.", pluginKey));
}
return file;
}
- private File newTempFile() {
+ private Path newTempFile() {
try {
- return File.createTempFile("fileCache", null, tempDir);
+ return Files.createTempFile(tempDir, "fileCache", null);
} catch (IOException e) {
throw new IllegalStateException("Fail to create temp file in " + tempDir, e);
}
}
- private static String computeMd5(File file) {
- try (InputStream fis = new BufferedInputStream(FileUtils.openInputStream(file))) {
+ private static String computeMd5(Path file) {
+ try (InputStream fis = new BufferedInputStream(Files.newInputStream(file))) {
return DigestUtils.md5Hex(fis);
} catch (IOException e) {
throw new IllegalStateException("Fail to compute md5 of " + file, e);
}
}
- private static void moveFile(File sourceFile, File targetFile) {
- boolean rename = sourceFile.renameTo(targetFile);
- // Check if the file was cached by another process during download
- if (!rename && !targetFile.exists()) {
- LOGGER.warn("Unable to rename {} to {}", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
- LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
- try {
- Files.move(sourceFile.toPath(), targetFile.toPath());
- } catch (IOException e) {
- throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
+ private static void moveFile(Path sourceFile, Path targetFile) {
+ try {
+ Files.move(sourceFile, targetFile, StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e1) {
+ // Check if the file was cached by another process during download
+ if (!Files.exists(targetFile)) {
+ LOGGER.warn("Unable to rename {} to {}", sourceFile, targetFile);
+ LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
+ try {
+ Files.move(sourceFile, targetFile);
+ } catch (IOException e2) {
+ throw new IllegalStateException("Fail to move " + sourceFile + " to " + targetFile, e2);
+ }
}
}
}
- private static void mkdir(File dir) {
+ private static void mkdir(Path dir) {
try {
- Files.createDirectories(dir.toPath());
+ Files.createDirectories(dir);
} catch (IOException e) {
throw new IllegalStateException("Fail to create cache directory: " + dir, e);
}
}
- private static File mkdir(File dir, String debugTitle) {
- if (!dir.isDirectory() || !dir.exists()) {
- LOGGER.debug("Create : {}", dir.getAbsolutePath());
+ private static Path mkdir(Path dir, String debugTitle) {
+ if (!Files.isDirectory(dir)) {
+ LOGGER.debug("Create : {}", dir);
try {
- Files.createDirectories(dir.toPath());
+ Files.createDirectories(dir);
} catch (IOException e) {
- throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
+ throw new IllegalStateException("Unable to create folder " + debugTitle + " at " + dir, e);
}
}
return dir;
}
- private static File locateHomeDir(Configuration configuration) {
- return Stream.of(
- configuration.get("sonar.userHome").orElse(null),
- System.getenv("SONAR_USER_HOME"),
- System.getProperty("user.home") + File.separator + ".sonar")
- .filter(Objects::nonNull)
- .findFirst()
- .map(File::new)
- .get();
- }
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java
index 9bf367f630d..081410361d6 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java
@@ -36,6 +36,7 @@ import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.plugin.PluginType;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.mediumtest.LocalPlugin;
import org.sonarqube.ws.client.GetRequest;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java
new file mode 100644
index 00000000000..afbc0bb3c18
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java
@@ -0,0 +1,35 @@
+/*
+ * 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.bootstrap;
+
+import java.nio.file.Path;
+
+public class SonarUserHome {
+
+ private final Path userHome;
+
+ public SonarUserHome(Path userHome) {
+ this.userHome = userHome;
+ }
+
+ public Path getPath() {
+ return userHome;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java
new file mode 100644
index 00000000000..da97e011ba8
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bootstrap;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
+import org.springframework.context.annotation.Bean;
+
+public class SonarUserHomeProvider {
+ private static final Logger LOG = LoggerFactory.getLogger(SonarUserHomeProvider.class);
+
+ private System2 system;
+
+ public SonarUserHomeProvider() {
+ this(new System2());
+ }
+
+ SonarUserHomeProvider(System2 system) {
+ this.system = system;
+ }
+
+ @Bean
+ public SonarUserHome provide(ScannerProperties scannerProps) {
+ Path home = findSonarHome(scannerProps);
+ LOG.debug("Sonar User Home: {}", home);
+ return new SonarUserHome(home);
+ }
+
+ private Path findSonarHome(ScannerProperties props) {
+ var home = props.property("sonar.userHome");
+ if (home != null) {
+ return Paths.get(home).toAbsolutePath();
+ }
+
+ home = system.envVariable("SONAR_USER_HOME");
+
+ if (home != null) {
+ return Paths.get(home).toAbsolutePath();
+ }
+
+ var userHome = Objects.requireNonNull(system.property("user.home"), "The system property 'user.home' is expected to be non null");
+ return Paths.get(userHome, ".sonar").toAbsolutePath();
+ }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java
index 69a430dd869..b9de4071df2 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java
@@ -48,6 +48,7 @@ import org.sonar.core.platform.SpringComponentContainer;
import org.sonar.core.util.DefaultHttpDownloader;
import org.sonar.core.util.UuidFactoryImpl;
import org.sonar.scanner.extension.ScannerCoreExtensionsInstaller;
+import org.sonar.scanner.http.ScannerWsClientProvider;
import org.sonar.scanner.notifications.DefaultAnalysisWarnings;
import org.sonar.scanner.platform.DefaultServer;
import org.sonar.scanner.repository.DefaultMetricsRepositoryLoader;
@@ -98,6 +99,7 @@ public class SpringGlobalContainer extends SpringComponentContainer {
DefaultServer.class,
DefaultDocumentationLinkGenerator.class,
new GlobalTempFolderProvider(),
+ new SonarUserHomeProvider(),
analysisWarnings,
UriReader.class,
PluginFiles.class,
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java
index f797a2cfdec..33037ede596 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java
@@ -30,7 +30,7 @@ import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
import org.sonar.core.util.Protobuf;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.protocol.internal.ScannerInternal.SensorCacheEntry;
import org.sonar.scanner.protocol.internal.SensorCacheData;
import org.sonar.scanner.scan.branch.BranchConfiguration;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java
index 1d192ad9584..a8093c868a7 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java
@@ -17,7 +17,7 @@
* 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.bootstrap;
+package org.sonar.scanner.http;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -39,6 +39,7 @@ import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
+import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
import org.sonarqube.ws.client.HttpException;
import org.sonarqube.ws.client.WsClient;
import org.sonarqube.ws.client.WsConnector;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java
index 280fb12de95..9b498867016 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java
@@ -17,7 +17,7 @@
* 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.bootstrap;
+package org.sonar.scanner.http;
import org.sonarqube.ws.client.WsRequest;
import org.sonarqube.ws.client.WsResponse;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
index 2d8f7328cd4..2511daf5d0f 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
@@ -17,16 +17,24 @@
* 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.bootstrap;
+package org.sonar.scanner.http;
import java.net.InetSocketAddress;
import java.net.Proxy;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Duration;
import java.time.format.DateTimeParseException;
+import nl.altindag.ssl.SSLFactory;
import org.sonar.api.CoreProperties;
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.sonar.scanner.http.ssl.CertificateStore;
+import org.sonar.scanner.http.ssl.SslConfig;
import org.sonarqube.ws.client.HttpConnector;
import org.sonarqube.ws.client.WsClientFactories;
import org.springframework.context.annotation.Bean;
@@ -52,7 +60,7 @@ public class ScannerWsClientProvider {
@Bean("DefaultScannerWsClient")
public DefaultScannerWsClient provide(ScannerProperties scannerProps, EnvironmentInformation env, GlobalAnalysisMode globalMode,
- System2 system, AnalysisWarnings analysisWarnings) {
+ System2 system, AnalysisWarnings analysisWarnings, SonarUserHome sonarUserHome) {
String url = defaultIfBlank(scannerProps.property("sonar.host.url"), "http://localhost:9000");
HttpConnector.Builder connectorBuilder = HttpConnector.newBuilder().acceptGzip(true);
@@ -63,13 +71,16 @@ public class ScannerWsClientProvider {
String envVarToken = defaultIfBlank(system.envVariable(TOKEN_ENV_VARIABLE), null);
String token = defaultIfBlank(scannerProps.property(TOKEN_PROPERTY), envVarToken);
String login = defaultIfBlank(scannerProps.property(CoreProperties.LOGIN), token);
+ var sslContext = configureSsl(parseSslConfig(scannerProps, sonarUserHome), system);
connectorBuilder
.readTimeoutMilliseconds(parseDurationProperty(socketTimeout, SONAR_SCANNER_SOCKET_TIMEOUT))
.connectTimeoutMilliseconds(parseDurationProperty(connectTimeout, SONAR_SCANNER_CONNECT_TIMEOUT))
.responseTimeoutMilliseconds(parseDurationProperty(responseTimeout, SONAR_SCANNER_RESPONSE_TIMEOUT))
.userAgent(env.toString())
.url(url)
- .credentials(login, scannerProps.property(CoreProperties.PASSWORD));
+ .credentials(login, scannerProps.property(CoreProperties.PASSWORD))
+ .setSSLSocketFactory(sslContext.getSslSocketFactory())
+ .setTrustManager(sslContext.getTrustManager().orElseThrow());
// OkHttp detects 'http.proxyHost' java property already, so just focus on sonar properties
String proxyHost = defaultIfBlank(scannerProps.property("sonar.scanner.proxyHost"), null);
@@ -109,4 +120,33 @@ public class ScannerWsClientProvider {
return parseIntProperty(propValue, propKey) * 1_000;
}
}
+
+ private static SslConfig parseSslConfig(ScannerProperties scannerProperties, SonarUserHome sonarUserHome) {
+ var keyStorePath = defaultIfBlank(scannerProperties.property("sonar.scanner.keystorePath"), sonarUserHome.getPath().resolve("ssl/keystore.p12").toString());
+ var keyStorePassword = defaultIfBlank(scannerProperties.property("sonar.scanner.keystorePassword"), CertificateStore.DEFAULT_PASSWORD);
+ var trustStorePath = defaultIfBlank(scannerProperties.property("sonar.scanner.truststorePath"), sonarUserHome.getPath().resolve("ssl/truststore.p12").toString());
+ var trustStorePassword = defaultIfBlank(scannerProperties.property("sonar.scanner.truststorePassword"), 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, System2 system2) {
+ var sslFactoryBuilder = SSLFactory.builder()
+ .withDefaultTrustMaterial()
+ .withSystemTrustMaterial();
+ if (system2.properties().containsKey("javax.net.ssl.keyStore")) {
+ sslFactoryBuilder.withSystemPropertyDerivedIdentityMaterial();
+ }
+ var keyStore = sslConfig.getKeyStore();
+ if (keyStore != null && Files.exists(keyStore.getPath())) {
+ sslFactoryBuilder.withIdentityMaterial(keyStore.getPath(), keyStore.getKeyStorePassword().toCharArray(), keyStore.getKeyStoreType());
+ }
+ var trustStore = sslConfig.getTrustStore();
+ if (trustStore != null && Files.exists(trustStore.getPath())) {
+ sslFactoryBuilder.withTrustMaterial(trustStore.getPath(), trustStore.getKeyStorePassword().toCharArray(), trustStore.getKeyStoreType());
+ }
+ return sslFactoryBuilder.build();
+ }
+
}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java
new file mode 100644
index 00000000000..349a6066fcb
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.scanner.http;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java
new file mode 100644
index 00000000000..b285d3bb478
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java
@@ -0,0 +1,48 @@
+/*
+ * 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.ssl;
+
+import java.nio.file.Path;
+
+public class CertificateStore {
+ public static final String DEFAULT_PASSWORD = "sonar";
+ public static final String DEFAULT_STORE_TYPE = "PKCS12";
+ private final Path path;
+ private final String keyStorePassword;
+ private final String keyStoreType;
+
+ public CertificateStore(Path path, String keyStorePassword) {
+ this.path = path;
+ this.keyStorePassword = keyStorePassword;
+ this.keyStoreType = DEFAULT_STORE_TYPE;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public String getKeyStoreType() {
+ return keyStoreType;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java
new file mode 100644
index 00000000000..8f172ca2534
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java
@@ -0,0 +1,43 @@
+/*
+ * 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.ssl;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class SslConfig {
+ private final CertificateStore keyStore;
+ private final CertificateStore trustStore;
+
+ public SslConfig(@Nullable CertificateStore keyStore, @Nullable CertificateStore trustStore) {
+ this.keyStore = keyStore;
+ this.trustStore = trustStore;
+ }
+
+ @CheckForNull
+ public CertificateStore getKeyStore() {
+ return keyStore;
+ }
+
+ @CheckForNull
+ public CertificateStore getTrustStore() {
+ return trustStore;
+ }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java
new file mode 100644
index 00000000000..e1e4c18147f
--- /dev/null
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.scanner.http.ssl;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java
index 9ec8ac9ae9c..a53f3de7f85 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java
@@ -26,7 +26,7 @@ import org.sonar.api.config.Configuration;
import org.sonar.api.platform.Server;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.platform.SonarQubeVersion;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java
index 7dce5a4bee5..aa4c30d352b 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java
@@ -27,7 +27,7 @@ import org.sonar.api.Startable;
import org.sonar.api.utils.MessageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
import org.sonar.scanner.report.CeTaskReportDataHolder;
import org.sonar.scanner.scan.ScanProperties;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java
index 2f1787885ad..15f12eef49d 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java
@@ -43,7 +43,7 @@ import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.TempFolder;
import org.sonar.api.utils.ZipUtils;
import org.sonar.core.ce.CeTaskCharacteristics;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
import org.sonar.scanner.ci.CiConfiguration;
import org.sonar.scanner.fs.InputModuleHierarchy;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java
index a69c13e2b0c..df65d94fa98 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java
@@ -25,7 +25,7 @@ import java.util.ArrayList;
import java.util.List;
import org.sonar.api.measures.Metric;
import org.sonar.api.measures.Metric.ValueType;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.protocol.GsonHelper;
import org.sonarqube.ws.client.GetRequest;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java
index 2a6b78be421..6403b5fcdaa 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java
@@ -21,7 +21,7 @@ package org.sonar.scanner.repository;
import java.io.IOException;
import java.io.InputStream;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.NewCodePeriods;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.HttpException;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java
index a35b66568e1..b6c681cdb12 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java
@@ -28,7 +28,7 @@ import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.impl.utils.ScannerUtils;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Batch.WsProjectResponse;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.HttpException;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java
index 30c716fc05a..cc5c2caa264 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java
@@ -28,7 +28,7 @@ import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import org.sonar.api.utils.MessageException;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Qualityprofiles.SearchWsResponse;
import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
import org.sonarqube.ws.client.GetRequest;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java
index 9b004f39e8b..58aeb9695f8 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.config.Configuration;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.client.GetRequest;
public class DefaultLanguagesLoader implements LanguagesLoader {
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java
index 6df3e293910..61b68ce9155 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java
@@ -34,7 +34,7 @@ import org.sonar.api.impl.utils.ScannerUtils;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Settings;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.HttpException;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java
index e5fabcfe48e..04d1feb1e95 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java
@@ -21,7 +21,7 @@ package org.sonar.scanner.repository.settings;
import java.util.Map;
import javax.inject.Inject;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
public class DefaultGlobalSettingsLoader extends AbstractSettingsLoader implements GlobalSettingsLoader {
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java
index e03e92d82e0..61b0d669248 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java
@@ -20,7 +20,7 @@
package org.sonar.scanner.repository.settings;
import java.util.Map;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.ScannerProperties;
public class DefaultProjectSettingsLoader extends AbstractSettingsLoader implements ProjectSettingsLoader {
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java
index 242ace13d31..1bc34c0198c 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java
@@ -31,7 +31,7 @@ import org.sonar.api.batch.rule.LoadedActiveRule;
import org.sonar.api.impl.utils.ScannerUtils;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.DateUtils;
-import org.sonar.scanner.bootstrap.ScannerWsClient;
+import org.sonar.scanner.http.ScannerWsClient;
import org.sonarqube.ws.Common.Paging;
import org.sonarqube.ws.Rules;
import org.sonarqube.ws.Rules.Active;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java
index 244fbe4aea9..2022782148c 100644
--- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java
+++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java
@@ -28,7 +28,7 @@ import org.sonar.api.CoreProperties;
import org.sonar.api.config.Configuration;
import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.batch.bootstrapper.EnvironmentInformation;
-import org.sonar.scanner.bootstrap.ScannerWsClientProvider;
+import org.sonar.scanner.http.ScannerWsClientProvider;
public class DeprecatedPropertiesWarningGenerator {
private static final Logger LOG = LoggerFactory.getLogger(DeprecatedPropertiesWarningGenerator.class);
diff --git a/sonar-scanner-engine/src/main/resources/logback.xml b/sonar-scanner-engine/src/main/resources/logback.xml
index c64f28abec7..ccd0dfe09b9 100644
--- a/sonar-scanner-engine/src/main/resources/logback.xml
+++ b/sonar-scanner-engine/src/main/resources/logback.xml
@@ -26,4 +26,9 @@
<logger name="org.springframework.context.annotation.AnnotationConfigApplicationContext" level="ERROR"/>
<logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
+
+ <!-- CertificateUtils is too verbose when loading system certificates -->
+ <logger name="nl.altindag.ssl.util.CertificateUtils" level="INFO"/>
+
+
</configuration> \ No newline at end of file
diff --git a/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml b/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
index ccf804462b6..37d6c262d1d 100644
--- a/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
+++ b/sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
@@ -36,4 +36,7 @@
<logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
+ <!-- CertificateUtils is too verbose when loading system certificates -->
+ <logger name="nl.altindag.ssl.util.CertificateUtils" level="INFO"/>
+
</configuration>
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java
index cac8b437689..6fb5829eb80 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java
@@ -24,7 +24,7 @@ import java.io.Reader;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.mockito.ArgumentMatcher;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.client.WsRequest;
import org.sonarqube.ws.client.WsResponse;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java
index 19a251859ae..15ee7b69856 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java
@@ -23,139 +23,107 @@ import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
-import java.util.Collections;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
-import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.sonar.api.CoreProperties;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.TempFolder;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class GlobalTempFolderProviderTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
+class GlobalTempFolderProviderTest {
- private GlobalTempFolderProvider tempFolderProvider = new GlobalTempFolderProvider();
+ private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
+ private final GlobalTempFolderProvider underTest = new GlobalTempFolderProvider();
@Test
- public void createTempFolderProps() throws Exception {
- File workingDir = temp.newFolder();
- workingDir.delete();
+ void createTempFolderProps(@TempDir Path workingDir) throws Exception {
+ Files.delete(workingDir);
- TempFolder tempFolder = tempFolderProvider.provide(
- new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
+ var tempFolder = underTest.provide(
+ new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.toAbsolutePath().toString())), sonarUserHome);
tempFolder.newDir();
tempFolder.newFile();
- assertThat(getCreatedTempDir(workingDir)).exists();
- assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
- FileUtils.deleteQuietly(workingDir);
+ assertThat(workingDir).isDirectory();
+ assertThat(workingDir.toFile().list()).hasSize(1);
+ var rootTmpDir = workingDir.toFile().listFiles()[0];
+ assertThat(rootTmpDir.list()).hasSize(2);
}
@Test
- public void cleanUpOld() throws IOException {
+ void cleanUpOld(@TempDir Path workingDir) throws IOException {
long creationTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(100);
- File workingDir = temp.newFolder();
for (int i = 0; i < 3; i++) {
- File tmp = new File(workingDir, ".sonartmp_" + i);
- tmp.mkdirs();
+ Path tmp = workingDir.resolve(".sonartmp_" + i);
+ Files.createDirectories(tmp);
setFileCreationDate(tmp, creationTime);
}
- tempFolderProvider.provide(
- new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
- // this also checks that all other temps were deleted
- assertThat(getCreatedTempDir(workingDir)).exists();
+ underTest.provide(
+ new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.toAbsolutePath().toString())), sonarUserHome);
- FileUtils.deleteQuietly(workingDir);
+ // this also checks that all other temps were deleted
+ assertThat(workingDir.toFile().list()).hasSize(1);
}
@Test
- public void createTempFolderSonarHome() throws Exception {
+ void createTempFolderFromSonarHome(@TempDir Path sonarUserHomePath) throws Exception {
// with sonar home, it will be in {sonar.home}/.sonartmp
- File sonarHome = temp.newFolder();
- File workingDir = new File(sonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
+ when(sonarUserHome.getPath()).thenReturn(sonarUserHomePath);
+
+ var expectedWorkingDir = sonarUserHomePath.resolve(CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
- TempFolder tempFolder = tempFolderProvider.provide(
- new ScannerProperties(ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath())));
+ TempFolder tempFolder = underTest.provide(new ScannerProperties(Map.of()), sonarUserHome);
tempFolder.newDir();
tempFolder.newFile();
- assertThat(getCreatedTempDir(workingDir)).exists();
- assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
- FileUtils.deleteQuietly(sonarHome);
+ assertThat(expectedWorkingDir).isDirectory();
+ assertThat(expectedWorkingDir.toFile().list()).hasSize(1);
+ var rootTmpDir = expectedWorkingDir.toFile().listFiles()[0];
+ assertThat(rootTmpDir.list()).hasSize(2);
}
@Test
- public void createTempFolderDefault() throws Exception {
- System2 system = mock(System2.class);
- tempFolderProvider = new GlobalTempFolderProvider(system);
- File userHome = temp.newFolder();
-
- when(system.envVariable("SONAR_USER_HOME")).thenReturn(null);
- when(system.property("user.home")).thenReturn(userHome.getAbsolutePath());
-
- // if nothing is defined, it will be in {user.home}/.sonar/.sonartmp
- File defaultSonarHome = new File(userHome.getAbsolutePath(), ".sonar");
- File workingDir = new File(defaultSonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
- try {
- TempFolder tempFolder = tempFolderProvider.provide(
- new ScannerProperties(Collections.emptyMap()));
- tempFolder.newDir();
- tempFolder.newFile();
- assertThat(getCreatedTempDir(workingDir)).exists();
- assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
- } finally {
- FileUtils.deleteQuietly(workingDir);
- }
- }
-
- @Test
- public void dotWorkingDir() {
- File sonarHome = temp.getRoot();
+ void dotWorkingDir(@TempDir Path sonarUserHomePath) {
+ when(sonarUserHome.getPath()).thenReturn(sonarUserHomePath);
String globalWorkDir = ".";
ScannerProperties globalProperties = new ScannerProperties(
- ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath(), CoreProperties.GLOBAL_WORKING_DIRECTORY, globalWorkDir));
+ ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, globalWorkDir));
- TempFolder tempFolder = tempFolderProvider.provide(globalProperties);
+ var tempFolder = underTest.provide(globalProperties, sonarUserHome);
File newFile = tempFolder.newFile();
- assertThat(newFile.getParentFile().getParentFile().getAbsolutePath()).isEqualTo(sonarHome.getAbsolutePath());
+
+ assertThat(newFile.getParentFile().getParentFile().toPath()).isEqualTo(sonarUserHomePath);
assertThat(newFile.getParentFile().getName()).startsWith(".sonartmp_");
}
@Test
- public void homeIsSymbolicLink() throws IOException {
+ void homeIsSymbolicLink(@TempDir Path realSonarHome, @TempDir Path symlink) throws IOException {
assumeTrue(!System2.INSTANCE.isOsWindows());
- File realSonarHome = temp.newFolder();
- File symlink = temp.newFolder();
- symlink.delete();
- Files.createSymbolicLink(symlink.toPath(), realSonarHome.toPath());
- ScannerProperties globalProperties = new ScannerProperties(ImmutableMap.of("sonar.userHome", symlink.getAbsolutePath()));
+ symlink.toFile().delete();
+ Files.createSymbolicLink(symlink, realSonarHome);
+ when(sonarUserHome.getPath()).thenReturn(symlink);
+
+ ScannerProperties globalProperties = new ScannerProperties(Map.of());
- TempFolder tempFolder = tempFolderProvider.provide(globalProperties);
+ TempFolder tempFolder = underTest.provide(globalProperties, sonarUserHome);
File newFile = tempFolder.newFile();
- assertThat(newFile.getParentFile().getParentFile().getAbsolutePath()).isEqualTo(symlink.getAbsolutePath());
+ assertThat(newFile.getParentFile().getParentFile().toPath().toAbsolutePath()).isEqualTo(symlink);
assertThat(newFile.getParentFile().getName()).startsWith(".sonartmp_");
}
- private File getCreatedTempDir(File workingDir) {
- assertThat(workingDir).isDirectory();
- assertThat(workingDir.listFiles()).hasSize(1);
- return workingDir.listFiles()[0];
- }
-
- private void setFileCreationDate(File f, long time) throws IOException {
- BasicFileAttributeView attributes = Files.getFileAttributeView(f.toPath(), BasicFileAttributeView.class);
+ private void setFileCreationDate(Path f, long time) throws IOException {
+ BasicFileAttributeView attributes = Files.getFileAttributeView(f, BasicFileAttributeView.class);
FileTime creationTime = FileTime.fromMillis(time);
attributes.setTimes(creationTime, creationTime, creationTime);
}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java
index 21d6b3c5317..907e13f92bc 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java
@@ -19,98 +19,113 @@
*/
package org.sonar.scanner.bootstrap;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;
-import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
-import okhttp3.HttpUrl;
-import okhttp3.mockwebserver.MockResponse;
-import okhttp3.mockwebserver.MockWebServer;
-import okio.Buffer;
import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.client.HttpConnector;
import org.sonarqube.ws.client.WsClientFactories;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+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.notFound;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.serverError;
+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 org.apache.commons.io.FileUtils.moveFile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static org.sonar.scanner.bootstrap.PluginFiles.PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY;
-public class PluginFilesTest {
+class PluginFilesTest {
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public MockWebServer server = new MockWebServer();
+ @RegisterExtension
+ static WireMockExtension sonarqube = WireMockExtension.newInstance()
+ .options(wireMockConfig().dynamicPort())
+ .build();
+
+ @TempDir
+ private Path tempDir;
+
+ private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
- private File userHome;
private PluginFiles underTest;
- @Before
- public void setUp() throws Exception {
- HttpConnector connector = HttpConnector.newBuilder().acceptGzip(true).url(server.url("/").toString()).build();
+ @BeforeEach
+ void setUp(@TempDir Path sonarUserHomeDir) throws Exception {
+ when(sonarUserHome.getPath()).thenReturn(sonarUserHomeDir);
+
+ HttpConnector connector = HttpConnector.newBuilder().acceptGzip(true).url(sonarqube.url("/")).build();
GlobalAnalysisMode analysisMode = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
DefaultScannerWsClient wsClient = new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connector), false,
analysisMode, analysisWarnings);
- userHome = temp.newFolder();
MapSettings settings = new MapSettings();
- settings.setProperty("sonar.userHome", userHome.getAbsolutePath());
settings.setProperty(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY, 1);
- underTest = new PluginFiles(wsClient, settings.asConfig());
+ underTest = new PluginFiles(wsClient, settings.asConfig(), sonarUserHome);
}
@Test
- public void get_jar_from_cache_if_present() throws Exception {
+ void get_jar_from_cache_if_present() throws Exception {
FileAndMd5 jar = createFileInCache("foo");
File result = underTest.get(newInstalledPlugin("foo", jar.md5)).get();
- verifySameContent(result, jar);
+ verifySameContent(result.toPath(), jar);
// no requests to server
- assertThat(server.getRequestCount()).isZero();
+ sonarqube.verify(0, anyRequestedFor(anyUrl()));
}
@Test
- public void download_and_add_jar_to_cache_if_missing() throws Exception {
+ void download_and_add_jar_to_cache_if_missing() throws Exception {
FileAndMd5 tempJar = new FileAndMd5();
- enqueueDownload(tempJar);
+ stubDownload(tempJar);
InstalledPlugin plugin = newInstalledPlugin("foo", tempJar.md5);
File result = underTest.get(plugin).get();
- verifySameContent(result, tempJar);
- HttpUrl requestedUrl = server.takeRequest().getRequestUrl();
- assertThat(requestedUrl.encodedPath()).isEqualTo("/api/plugins/download");
- assertThat(requestedUrl.encodedQuery()).isEqualTo("plugin=foo");
+ verifySameContent(result.toPath(), tempJar);
+
+ sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
// get from cache on second call
result = underTest.get(plugin).get();
- verifySameContent(result, tempJar);
- assertThat(server.getRequestCount()).isOne();
+ verifySameContent(result.toPath(), tempJar);
+
+ sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
}
@Test
- public void return_empty_if_plugin_not_found_on_server() {
- server.enqueue(new MockResponse().setResponseCode(404));
+ void return_empty_if_plugin_not_found_on_server() {
+ sonarqube.stubFor(get(anyUrl())
+ .willReturn(notFound()));
InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
Optional<File> result = underTest.get(plugin);
@@ -119,9 +134,9 @@ public class PluginFilesTest {
}
@Test
- public void fail_if_integrity_of_download_is_not_valid() throws IOException {
+ void fail_if_integrity_of_download_is_not_valid() throws IOException {
FileAndMd5 tempJar = new FileAndMd5();
- enqueueDownload(tempJar.file, "invalid_hash");
+ stubDownload(tempJar.file, "invalid_hash");
InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
expectISE("foo", "was expected to have checksum invalid_hash but had " + tempJar.md5,
@@ -129,63 +144,68 @@ public class PluginFilesTest {
}
@Test
- public void fail_if_md5_header_is_missing_from_response() throws IOException {
- File tempJar = temp.newFile();
- enqueueDownload(tempJar, null);
+ void fail_if_md5_header_is_missing_from_response(@TempDir Path tempDir) throws IOException {
+ var tempJar = Files.createTempFile(tempDir, "plugin", ".jar");
+ stubDownload(tempJar, null);
InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
expectISE("foo", "did not return header Sonar-MD5", () -> underTest.get(plugin));
}
@Test
- public void fail_if_server_returns_error() {
- server.enqueue(new MockResponse().setResponseCode(500));
+ void fail_if_server_returns_error() {
+ sonarqube.stubFor(get(anyUrl())
+ .willReturn(serverError()));
+
InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
expectISE("foo", "returned code 500", () -> underTest.get(plugin));
}
@Test
- public void getPlugin_whenTimeOutReached_thenDownloadFails() {
- MockResponse response = new MockResponse().setBody("test").setBodyDelay(2, TimeUnit.SECONDS);
- response.setHeader("Sonar-MD5", "md5");
- server.enqueue(response);
+ void getPlugin_whenTimeOutReached_thenDownloadFails() {
+ sonarqube.stubFor(get(anyUrl())
+ .willReturn(ok()
+ .withFixedDelay(5000)));
+
InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
assertThatThrownBy(() -> underTest.get(plugin))
.isInstanceOf(IllegalStateException.class)
- .hasMessageStartingWith("Fail to download plugin [" + plugin.key + "]")
+ .hasMessageStartingWith("Fail to request url")
.cause().isInstanceOf(SocketTimeoutException.class);
}
@Test
- public void download_a_new_version_of_plugin_during_blue_green_switch() throws IOException {
+ void download_a_new_version_of_plugin_during_blue_green_switch() throws IOException {
FileAndMd5 tempJar = new FileAndMd5();
- enqueueDownload(tempJar);
+ stubDownload(tempJar);
// expecting to download plugin foo with checksum "abc"
InstalledPlugin pluginV1 = newInstalledPlugin("foo", "abc");
File result = underTest.get(pluginV1).get();
- verifySameContent(result, tempJar);
+ verifySameContent(result.toPath(), tempJar);
// new version of downloaded jar is put in cache with the new md5
InstalledPlugin pluginV2 = newInstalledPlugin("foo", tempJar.md5);
result = underTest.get(pluginV2).get();
- verifySameContent(result, tempJar);
- assertThat(server.getRequestCount()).isOne();
+ verifySameContent(result.toPath(), tempJar);
+
+ sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
// v1 still requests server and downloads v2
- enqueueDownload(tempJar);
+ stubDownload(tempJar);
result = underTest.get(pluginV1).get();
- verifySameContent(result, tempJar);
- assertThat(server.getRequestCount()).isEqualTo(2);
+ verifySameContent(result.toPath(), tempJar);
+
+ sonarqube.verify(exactly(2), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
}
@Test
- public void fail_if_cached_file_is_outside_cache_dir() throws IOException {
+ void fail_if_cached_file_is_outside_cache_dir() throws IOException {
FileAndMd5 tempJar = new FileAndMd5();
- enqueueDownload(tempJar);
+ stubDownload(tempJar);
InstalledPlugin plugin = newInstalledPlugin("foo/bar", "abc");
@@ -200,29 +220,29 @@ public class PluginFilesTest {
}
private FileAndMd5 moveToCache(String pluginKey, FileAndMd5 jar) throws IOException {
- File jarInCache = new File(userHome, "cache/" + jar.md5 + "/sonar-" + pluginKey + "-plugin.jar");
- moveFile(jar.file, jarInCache);
+ Path jarInCache = sonarUserHome.getPath().resolve("cache/" + jar.md5 + "/sonar-" + pluginKey + "-plugin.jar");
+ moveFile(jar.file.toFile(), jarInCache.toFile());
return new FileAndMd5(jarInCache, jar.md5);
}
/**
* Enqueue download of file with valid MD5
*/
- private void enqueueDownload(FileAndMd5 file) throws IOException {
- enqueueDownload(file.file, file.md5);
+ private void stubDownload(FileAndMd5 file) throws IOException {
+ stubDownload(file.file, file.md5);
}
/**
* Enqueue download of file with a MD5 that may not be returned (null) or not valid
*/
- private void enqueueDownload(File file, @Nullable String md5) throws IOException {
- Buffer body = new Buffer();
- body.write(FileUtils.readFileToByteArray(file));
- MockResponse response = new MockResponse().setBody(body);
+ private void stubDownload(Path file, @Nullable String md5) throws IOException {
+ var responseDefBuilder = ok();
if (md5 != null) {
- response.setHeader("Sonar-MD5", md5);
+ responseDefBuilder.withHeader("Sonar-MD5", md5);
}
- server.enqueue(response);
+ responseDefBuilder.withBody(Files.readAllBytes(file));
+ sonarqube.stubFor(get(urlMatching("/api/plugins/download\\?plugin=.*"))
+ .willReturn(responseDefBuilder));
}
private static InstalledPlugin newInstalledPlugin(String pluginKey, String fileChecksum) {
@@ -232,10 +252,10 @@ public class PluginFilesTest {
return plugin;
}
- private static void verifySameContent(File file1, FileAndMd5 file2) {
- assertThat(file1).isFile().exists();
- assertThat(file2.file).isFile().exists();
- assertThat(file1).hasSameContentAs(file2.file);
+ private static void verifySameContent(Path file1, FileAndMd5 file2) {
+ assertThat(file1).isRegularFile();
+ assertThat(file2.file).isRegularFile();
+ assertThat(file1).hasSameTextualContentAs(file2.file);
}
private void expectISE(String pluginKey, String message, ThrowingCallable shouldRaiseThrowable) {
@@ -246,18 +266,18 @@ public class PluginFilesTest {
}
private class FileAndMd5 {
- private final File file;
+ private final Path file;
private final String md5;
- FileAndMd5(File file, String md5) {
+ FileAndMd5(Path file, String md5) {
this.file = file;
this.md5 = md5;
}
FileAndMd5() throws IOException {
- this.file = temp.newFile();
- FileUtils.write(this.file, RandomStringUtils.random(3));
- try (InputStream fis = FileUtils.openInputStream(this.file)) {
+ this.file = Files.createTempFile(tempDir, "jar", null);
+ Files.write(this.file, RandomStringUtils.random(3).getBytes());
+ try (InputStream fis = Files.newInputStream(this.file)) {
this.md5 = DigestUtils.md5Hex(fis);
} catch (IOException e) {
throw new IllegalStateException("Fail to compute md5 of " + this.file, e);
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java
index 0d27bcb284b..210c5228bd9 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java
@@ -35,6 +35,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.scanner.WsTestUtil;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java
deleted file mode 100644
index 2e63d4c0139..00000000000
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * 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.bootstrap;
-
-import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInfo;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.sonar.api.notifications.AnalysisWarnings;
-import org.sonar.api.utils.System2;
-import org.sonar.batch.bootstrapper.EnvironmentInformation;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.HttpConnector;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
-import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-class ScannerWsClientProviderTest {
-
- public static final GlobalAnalysisMode GLOBAL_ANALYSIS_MODE = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
- public static final AnalysisWarnings ANALYSIS_WARNINGS = warning -> {
- };
- private final Map<String, String> scannerProps = new HashMap<>();
-
- private final ScannerWsClientProvider underTest = new ScannerWsClientProvider();
- private final EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
- public static final String PROXY_AUTH_ENABLED = "proxy-auth";
-
- @RegisterExtension
- static WireMockExtension sonarqubeMock = WireMockExtension.newInstance()
- .options(wireMockConfig().dynamicPort())
- .build();
-
- @RegisterExtension
- static WireMockExtension proxyMock = WireMockExtension.newInstance()
- .options(wireMockConfig().dynamicPort())
- .build();
-
- private final System2 system2 = mock(System2.class);
- private final Properties systemProps = new Properties();
-
- @BeforeEach
- void configureMocks(TestInfo info) {
- when(system2.properties()).thenReturn(systemProps);
-
- if (info.getTags().contains(PROXY_AUTH_ENABLED)) {
- proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
- .inScenario("Proxy Auth")
- .whenScenarioStateIs(STARTED)
- .willReturn(aResponse()
- .withStatus(407)
- .withHeader("Proxy-Authenticate", "Basic realm=\"Access to the proxy\""))
- .willSetStateTo("Challenge returned"));
- proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
- .inScenario("Proxy Auth")
- .whenScenarioStateIs("Challenge returned")
- .willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
- } else {
- proxyMock.stubFor(get(urlMatching("/api/plugins/.*")).willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
- }
- }
-
- @Test
- void provide_client_with_default_settings() {
- DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
- assertThat(client).isNotNull();
- assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/");
- 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);
-
- // Proxy is not accessed
- assertThat(proxyMock.findAllUnmatchedRequests()).isEmpty();
- }
-
- @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);
-
- assertThat(client).isNotNull();
- HttpConnector httpConnector = (HttpConnector) client.wsConnector();
- assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/");
- assertThat(httpConnector.okHttpClient().proxy()).isNull();
- }
-
- @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);
-
- 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);
-
- 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_honor_scanner_proxy_settings() {
- scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
- scannerProps.put("sonar.scanner.proxyHost", "localhost");
- scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
-
- DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
- sonarqubeMock.stubFor(get("/api/plugins/installed")
- .willReturn(aResponse().withStatus(200).withBody("Success")));
-
- HttpConnector httpConnector = (HttpConnector) client.wsConnector();
- try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
- assertThat(r.code()).isEqualTo(200);
- assertThat(r.content()).isEqualTo("Success");
- }
-
- proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed")));
- }
-
- @Test
- void it_should_throw_if_invalid_proxy_port() {
- scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
- scannerProps.put("sonar.scanner.proxyHost", "localhost");
- scannerProps.put("sonar.scanner.proxyPort", "not_a_number");
- var scannerPropertiesBean = new ScannerProperties(scannerProps);
-
- assertThrows(IllegalArgumentException.class, () -> underTest.provide(scannerPropertiesBean, env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS));
- }
-
- @Test
- @Tag(PROXY_AUTH_ENABLED)
- void it_should_honor_scanner_proxy_settings_with_auth() {
- var proxyLogin = "proxyLogin";
- var proxyPassword = "proxyPassword";
- scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
- scannerProps.put("sonar.scanner.proxyHost", "localhost");
- scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
- scannerProps.put("sonar.scanner.proxyUser", proxyLogin);
- scannerProps.put("sonar.scanner.proxyPassword", proxyPassword);
-
- DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
- sonarqubeMock.stubFor(get("/api/plugins/installed")
- .willReturn(aResponse().withStatus(200).withBody("Success")));
-
- HttpConnector httpConnector = (HttpConnector) client.wsConnector();
- try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
- assertThat(r.code()).isEqualTo(200);
- assertThat(r.content()).isEqualTo("Success");
- }
-
- proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
- .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
-
- }
-
- @Test
- @Tag(PROXY_AUTH_ENABLED)
- void it_should_honor_old_jvm_proxy_auth_properties() {
- var proxyLogin = "proxyLogin";
- var proxyPassword = "proxyPassword";
- scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
- scannerProps.put("sonar.scanner.proxyHost", "localhost");
- scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
- systemProps.put("http.proxyUser", proxyLogin);
- systemProps.put("http.proxyPassword", proxyPassword);
-
- DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
- sonarqubeMock.stubFor(get("/api/plugins/installed")
- .willReturn(aResponse().withStatus(200).withBody("Success")));
-
- HttpConnector httpConnector = (HttpConnector) client.wsConnector();
- try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
- assertThat(r.code()).isEqualTo(200);
- assertThat(r.content()).isEqualTo("Success");
- }
-
- proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
- .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
-
- }
-}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java
new file mode 100644
index 00000000000..e499029b6bc
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.bootstrap;
+
+import java.nio.file.Path;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.api.utils.System2;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SonarUserHomeProviderTest {
+
+ private final System2 system = mock(System2.class);
+ private final SonarUserHomeProvider underTest = new SonarUserHomeProvider(system);
+
+ @Test
+ void createTempFolderFromDefaultUserHome(@TempDir Path userHome) {
+ when(system.envVariable("SONAR_USER_HOME")).thenReturn(null);
+ when(system.property("user.home")).thenReturn(userHome.toString());
+
+ var sonarUserHome = underTest.provide(new ScannerProperties(Map.of()));
+
+ assertThat(sonarUserHome.getPath()).isEqualTo(userHome.resolve(".sonar"));
+ }
+
+ @Test
+ void should_consider_env_variable_over_user_home(@TempDir Path userHome, @TempDir Path sonarUserHomeFromEnv) {
+ when(system.envVariable("SONAR_USER_HOME")).thenReturn(sonarUserHomeFromEnv.toString());
+ when(system.property("user.home")).thenReturn(userHome.toString());
+
+ var sonarUserHome = underTest.provide(new ScannerProperties(Map.of()));
+
+ assertThat(sonarUserHome.getPath()).isEqualTo(sonarUserHomeFromEnv);
+ }
+
+ @Test
+ void should_consider_scanner_property_over_env_and_user_home(@TempDir Path userHome, @TempDir Path sonarUserHomeFromEnv, @TempDir Path sonarUserHomeFromProps) {
+ when(system.envVariable("SONAR_USER_HOME")).thenReturn(sonarUserHomeFromEnv.toString());
+ when(system.property("user.home")).thenReturn(userHome.toString());
+
+ var sonarUserHome = underTest.provide(new ScannerProperties(Map.of("sonar.userHome", sonarUserHomeFromProps.toString())));
+
+ assertThat(sonarUserHome.getPath()).isEqualTo(sonarUserHomeFromProps);
+ }
+
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java
index 568c6d00aac..aaf4783ac61 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java
@@ -34,7 +34,7 @@ import org.mockito.ArgumentCaptor;
import org.sonar.api.scanner.fs.InputProject;
import org.sonar.api.utils.MessageException;
import org.sonar.api.testfixtures.log.LogTester;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.protocol.internal.ScannerInternal.SensorCacheEntry;
import org.sonar.scanner.protocol.internal.SensorCacheData;
import org.sonar.scanner.scan.branch.BranchConfiguration;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java
index 07bcae141a7..95f98c49cbe 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java
@@ -17,7 +17,7 @@
* 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.bootstrap;
+package org.sonar.scanner.http;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@@ -33,6 +33,8 @@ import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
+import org.sonar.scanner.bootstrap.ScannerProperties;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.HttpException;
import org.sonarqube.ws.client.MockWsResponse;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java
new file mode 100644
index 00000000000..e0ee3121acc
--- /dev/null
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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<String, String> 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);
+ }
+ }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java
index 0a4b1adc125..b9bb8f112b8 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java
@@ -24,7 +24,7 @@ import org.sonar.api.CoreProperties;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.utils.Version;
import org.sonar.core.platform.SonarQubeVersion;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java
index 89013ebcbc9..0e7e67cb5c5 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java
@@ -33,7 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.event.Level;
import org.sonar.api.testfixtures.log.LogTesterJUnit5;
import org.sonar.api.utils.MessageException;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
import org.sonar.scanner.report.CeTaskReportDataHolder;
import org.sonar.scanner.scan.ScanProperties;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java
index 58a6f9dd9cd..9d05effedbf 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java
@@ -39,7 +39,7 @@ import org.sonar.api.platform.Server;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.TempFolder;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
import org.sonar.scanner.ci.CiConfiguration;
import org.sonar.scanner.ci.DevOpsPlatformInfo;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java
index 128e4ffb23a..f0b61cf71dd 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java
@@ -26,7 +26,7 @@ import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Test;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java
index 2fe7f09ae84..856bb82823a 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java
@@ -25,7 +25,7 @@ import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.NewCodePeriods;
import static org.mockito.Mockito.mock;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java
index cf0391cc770..e55fb9880d0 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java
@@ -30,7 +30,7 @@ import org.junit.Test;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.utils.MessageException;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Batch.WsProjectResponse;
import org.sonarqube.ws.client.HttpException;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java
index fac637b5860..8c0253fbe9d 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java
@@ -26,7 +26,7 @@ import java.io.InputStream;
import org.junit.Test;
import org.sonar.api.utils.MessageException;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Qualityprofiles;
import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
import org.sonarqube.ws.client.HttpException;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java
index 6125ce99076..294e4763d2e 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java
@@ -28,7 +28,7 @@ import org.slf4j.event.Level;
import org.sonar.api.config.Configuration;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java
index 0e6e74013d8..0b3d1bb23b1 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java
@@ -25,7 +25,7 @@ import java.io.PipedOutputStream;
import java.util.Map;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonarqube.ws.Settings;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.WsResponse;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java
index 417be96ba99..c1694faa9a1 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java
@@ -25,7 +25,7 @@ import java.io.PipedOutputStream;
import java.util.Map;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.bootstrap.ScannerProperties;
import org.sonarqube.ws.Settings;
import org.sonarqube.ws.client.GetRequest;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java
index 1ba8fd8d93a..bae17ee866d 100644
--- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java
+++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java
@@ -30,7 +30,7 @@ import org.sonar.api.batch.rule.LoadedActiveRule;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Rules;
diff --git a/sonar-scanner-engine/src/test/resources/ssl/README.md b/sonar-scanner-engine/src/test/resources/ssl/README.md
new file mode 100644
index 00000000000..140d49a624b
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/README.md
@@ -0,0 +1,452 @@
+## Let's create TLS certificates
+
+The most common format of certificates are PEM, so let's generate them instead of using Java keytool (that can also generate keys in JKS format).
+
+This README, is a *simplified* version for generating the certificates only for test's purposes.
+
+**DO NOT USE IT FOR PRODUCTION**
+
+### Generation of a TLS server certificate
+
+In this example the configuration of OpenSSL is entirely in openssl.conf (a stripped version of openssl.cnf that may vary from distribution to distribution)
+
+#### First let's create a Certificate Authority
+
+The Certificate Authority is a private key that is used to sign other X.509 certificates in order to validate the ownership of a website (trusted tier).
+
+```bash
+$ openssl genrsa -out ca.key 4096
+.....++
+................................................................................................................................................++
+e is 65537 (0x010001)
+```
+
+Now we have our key to sign other certificates : `ca.key` in PEM format.
+
+Let's create our X.509 CA certificate :
+
+```bash
+$ openssl req -key ca.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca.crt -config ./openssl.conf
+You are about to be asked to enter information that will be incorporated
+into your certificate request.
+What you are about to enter is what is called a Distinguished Name or a DN.
+There are quite a few fields but you can leave some blank
+For some fields there will be a default value,
+If you enter '.', the field will be left blank.
+-----
+Country Name (2-letter code) [CH]:
+State or Province Name (full name) [Geneva]:
+Locality (e.g. city name) [Geneva]:
+Organization (e.g. company name) [SonarSource SA]:
+Common Name (your.domain.com) [localhost]:
+```
+
+There is no important values here.
+
+#### Let's create a self-signed certificate our TLS server using our CA
+
+We want to create a X.509 certificate for our https server. This certificate will be a Certificate Signing Request. A certificate that need to be signed by a trusted tier.
+The default configuration is set in `openssl.conf` and it has been configuration for `localhost`.
+The most important part is the `Common Name` and `DNS.1` (set in `openssl.conf`).
+
+So just keep using enter with this command line :
+
+```bash
+$ openssl req -new -keyout server.key -out server.csr -nodes -newkey rsa:4096 -config ./openssl.conf
+ Generating a 4096 bit RSA private key
+ ........................................................................++
+ .........................................................................................++
+ writing new private key to 'server.key'
+ -----
+ You are about to be asked to enter information that will be incorporated
+ into your certificate request.
+ What you are about to enter is what is called a Distinguished Name or a DN.
+ There are quite a few fields but you can leave some blank
+ For some fields there will be a default value,
+ If you enter '.', the field will be left blank.
+ -----
+ Country Name (2-letter code) [CH]:
+ State or Province Name (full name) [Geneva]:
+ Locality (e.g. city name) [Geneva]:
+ Organization (e.g. company name) [SonarSource SA]:
+ Common Name (your.domain.com) [localhost]:
+```
+
+No we have `server.csr` file valid for 10 years.
+Let's see what's in this certificate :
+```bash
+$ openssl req -verify -in server.csr -text -noout
+verify OK
+Certificate Request:
+ Data:
+ Version: 1 (0x0)
+ Subject: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (4096 bit)
+ Modulus:
+ 00:c8:2d:dc:64:1a:b6:d9:a9:3e:bd:3f:d3:ae:27:
+ ab:00:a8:09:f7:9e:ae:b5:70:c0:11:ab:2d:45:48:
+ 6c:b9:b3:b1:4b:42:b7:4e:48:d3:2e:38:cb:e5:7d:
+ 14:30:d3:b8:1d:2f:e2:09:04:cc:aa:80:09:51:bc:
+ 59:9d:a7:7a:76:34:cc:7a:2b:ae:d3:ef:98:38:ef:
+ b2:8a:0e:e9:2f:79:4e:d4:a9:10:63:2b:5b:05:05:
+ ef:6b:98:41:e3:c0:3e:6c:5f:8a:66:10:ca:98:e5:
+ 37:c6:ea:13:48:c9:92:22:53:44:1a:61:27:f4:60:
+ 16:a7:a9:87:a9:d3:cf:88:5e:d4:47:44:24:4f:6d:
+ 5e:c0:4a:ff:ad:e4:82:63:da:82:eb:9e:b3:76:6f:
+ 5d:b4:2d:fc:96:4a:98:e4:f5:20:97:48:38:11:29:
+ 33:7d:5a:96:fa:28:49:9f:cb:24:f8:02:f6:bb:ed:
+ f3:91:90:51:10:c2:93:28:56:6e:4d:51:51:10:27:
+ 8f:c3:f0:cd:ee:51:2d:dc:e5:a7:21:55:20:44:ac:
+ 8b:66:1d:b7:eb:e0:ed:69:f0:d4:32:82:ee:53:91:
+ 3b:ee:58:83:ba:3b:9d:3f:f7:23:0e:36:46:20:6b:
+ 6a:80:9b:11:46:28:39:60:25:69:9e:e5:d0:34:ba:
+ 2b:c3:33:f2:44:3d:fb:8f:2d:47:a6:ae:64:9a:b3:
+ 5a:f0:ed:cb:3e:86:33:80:23:32:d0:e7:51:91:a8:
+ c6:97:d1:7c:e4:02:52:5d:7c:a9:97:83:00:c5:10:
+ fb:13:f9:29:1f:79:c4:a5:8c:7b:64:e0:cd:b6:a1:
+ 34:36:aa:f4:63:63:77:12:d3:fa:fe:1d:54:2e:64:
+ 43:38:a2:71:28:72:7a:bf:33:cb:8c:27:a7:66:51:
+ 8f:6f:e8:d2:90:19:2f:d4:8e:ac:b4:7b:e0:53:a8:
+ 0f:11:d1:7d:08:71:de:0a:a4:63:10:79:c8:e8:bf:
+ 7e:be:8b:06:7d:43:9b:4b:a1:0a:49:a6:c8:c6:43:
+ c4:24:23:13:2a:b2:f9:f2:b8:e7:8e:ab:3e:2a:b5:
+ 50:26:23:d6:b2:d3:ee:23:ec:d1:36:92:70:2e:df:
+ 82:6a:d2:07:bb:f0:97:51:42:e4:d8:49:69:35:bb:
+ 38:90:1f:8e:aa:1d:27:78:26:26:d4:36:75:ee:83:
+ 17:69:cb:7f:53:45:8f:b4:63:13:d5:fd:42:10:8a:
+ d3:75:38:4a:bd:13:cf:68:5e:41:6d:f0:57:b5:75:
+ e3:dc:10:82:ab:29:ed:a1:27:9c:50:74:f2:4c:4a:
+ a3:78:2a:53:ca:90:a6:89:20:24:85:b5:ec:c9:c7:
+ be:96:b5
+ Exponent: 65537 (0x10001)
+ Attributes:
+ Requested Extensions:
+ X509v3 Subject Alternative Name:
+ DNS:localhost
+ X509v3 Key Usage:
+ Digital Signature, Key Encipherment, Data Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ Signature Algorithm: sha256WithRSAEncryption
+ bf:9d:6e:2f:cc:40:9b:92:29:c2:f1:0a:85:6c:35:eb:8e:fa:
+ 13:0c:53:58:33:5f:7b:09:58:5f:dd:94:7e:2c:65:ed:73:91:
+ 2a:6b:cc:2d:ec:26:1c:8e:95:57:d9:35:19:82:4f:42:59:81:
+ d9:b7:bb:08:70:28:70:35:50:f6:6a:46:e0:2a:ab:90:50:5a:
+ dc:b0:c3:b8:52:d7:5c:90:8f:4c:61:09:2c:ba:4a:31:37:6f:
+ e0:b9:6b:98:dd:aa:dd:52:66:7e:06:f1:8a:4b:bc:23:0d:62:
+ d3:b9:86:8f:3e:cc:05:2b:4d:c4:ad:cf:ae:be:33:22:f6:95:
+ 00:f0:36:96:26:5e:42:84:d0:2a:79:41:1e:18:10:1c:96:3e:
+ 9a:8b:cc:a5:f9:59:5b:78:d0:a1:a5:2e:4d:55:30:10:0b:cd:
+ 13:bc:75:9a:49:e0:de:a4:4d:ed:9b:e8:42:2f:74:2b:dc:6f:
+ 2d:d3:38:a9:e8:f8:98:2c:56:aa:3e:dd:0d:48:78:16:4c:50:
+ fd:0a:b3:3c:28:ac:64:7e:e9:bb:10:0e:3b:29:68:40:a9:19:
+ 5a:2c:5c:d6:7e:32:39:96:49:a7:4c:6a:a6:09:8e:d4:b8:1e:
+ 3e:2c:93:c3:2c:da:f2:09:20:ef:f4:a9:d2:ff:de:cd:7b:20:
+ 66:46:ff:c2:36:c3:7d:32:d6:55:d1:fe:0f:00:9a:23:56:97:
+ 52:a1:0a:52:64:29:50:c7:5d:b4:1e:e4:67:9a:07:3f:fb:85:
+ 03:00:22:d8:f5:e6:bc:95:bf:bc:08:ab:4d:32:4c:d6:52:e0:
+ 72:3e:8a:a5:85:72:43:d6:d4:51:6e:99:9a:1f:d8:0e:fd:4d:
+ 59:81:7e:c1:81:6d:3b:69:76:ce:53:a4:c0:69:46:72:b2:fe:
+ 40:b3:a5:5c:b0:ce:d2:61:83:be:0f:c3:85:a0:21:a7:e8:fd:
+ 2f:2c:1c:68:24:1d:9b:a3:43:cb:5e:30:21:af:e8:2e:4e:ec:
+ ea:a7:d2:68:f1:bd:3f:3c:41:48:ac:91:f9:9d:e8:f2:3d:cb:
+ d0:82:d2:00:ed:7b:fa:d8:98:e3:a8:74:f2:ce:70:95:0a:9d:
+ c2:b2:cc:08:d1:fd:de:26:d3:3e:c0:62:28:9b:b4:2d:f4:b5:
+ 6d:48:c9:d3:05:f5:1e:68:17:6b:fb:02:2e:20:98:1a:de:d4:
+ ae:6b:e0:68:97:98:e0:4f:47:ec:14:fd:dc:57:d2:e2:5c:59:
+ 36:a5:0b:94:b7:4e:b8:ae:ee:c9:ac:02:ae:43:bf:9f:07:da:
+ 0c:44:b0:47:69:1d:64:ea:bd:68:af:4f:a7:9a:1f:b1:b9:1d:
+ 71:0e:86:4e:0c:ff:a3:1d
+```
+
+#### Let's sign this certificate with our own CA
+
+The CSR will be signed with our previously created ca.key
+We'll sign it to be valid for 10years (3650)
+
+```bash
+$ openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.pem -sha256 -extfile v3.ext
+Signature ok
+subject=C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+Getting CA Private Key
+```
+
+Let's verify what are in this certificate :
+
+```bash
+$ openssl x509 -in server.pem -text -noout
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ d5:c5:2a:c2:c8:f6:43:c7
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = SonarSource SA
+ Validity
+ Not Before: Mar 17 14:12:29 2020 GMT
+ Not After : Mar 15 14:12:29 2030 GMT
+ Subject: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (4096 bit)
+ Modulus:
+ 00:a2:43:1e:8b:60:b5:e0:61:3e:99:a4:54:93:c8:
+ 16:14:c2:fa:fd:e5:7c:05:02:71:09:46:d9:2a:52:
+ 57:12:d7:74:46:6a:bd:d4:de:4a:06:b2:51:83:2c:
+ 98:07:8c:b0:f7:e1:8a:aa:fc:0c:30:c6:d7:ec:57:
+ 0b:a7:12:45:e3:13:1a:26:e8:22:d8:fd:2a:9e:ae:
+ 7b:20:b8:41:99:50:0e:b7:1c:bb:78:18:60:25:67:
+ 78:5b:af:d8:7f:d1:01:12:81:0a:1f:dd:f0:54:bc:
+ 57:16:05:22:7c:65:a2:7e:03:ed:e8:7f:50:b1:cd:
+ 7c:e8:7b:58:cb:df:6d:e3:04:03:78:a4:83:e7:20:
+ c4:37:bc:00:ba:7c:12:d9:ac:52:88:88:72:df:fc:
+ 35:8f:94:f0:1b:33:f8:94:b8:bc:ab:0e:89:68:5f:
+ 92:1b:af:c9:da:c2:c2:e2:a1:c3:8e:c8:16:1a:9e:
+ 89:7a:b4:24:2c:24:df:c5:26:59:ab:d8:f9:06:39:
+ 02:c0:0d:88:5a:0c:14:e7:bc:c5:b8:4c:e5:e0:85:
+ b2:0b:88:36:b3:d5:35:10:e9:b8:5a:48:69:1a:b3:
+ 2a:4a:d6:f3:f5:6a:91:41:f8:1e:da:d0:0e:21:c3:
+ a2:f8:5c:08:42:a2:2b:13:be:63:e5:67:d5:19:2f:
+ 2c:96:6d:17:1c:7f:34:19:68:cf:91:b6:14:d9:9a:
+ 1b:1c:f9:08:d7:f9:2d:c3:48:14:3d:02:d4:90:f7:
+ f2:74:65:f8:22:2d:46:b2:76:cd:46:c1:8e:ab:a1:
+ 11:d7:12:14:77:e3:1c:c3:1c:fa:32:79:0e:0e:59:
+ 55:e4:9d:60:d7:18:0b:25:82:97:28:30:df:de:89:
+ 5b:56:37:a2:33:86:26:12:83:75:f0:02:ae:88:b5:
+ d6:5e:a2:b7:e7:57:9d:de:72:ad:d6:55:2a:e1:a8:
+ 4c:15:18:a9:e3:22:52:f1:74:e1:b0:d2:e7:9b:ec:
+ f9:6d:5f:86:c2:9c:e2:22:f2:f4:11:a2:d1:71:b8:
+ 77:e4:8c:4c:ed:84:e8:f9:82:a2:f1:73:95:19:08:
+ 92:d5:b3:50:be:bc:c2:ec:0e:d7:da:53:d2:22:36:
+ c8:d8:48:d1:22:0d:42:a7:68:6d:e5:b6:5f:00:7d:
+ 70:e4:5f:fe:df:db:3a:96:30:c8:76:89:e9:d1:98:
+ 1e:63:e2:d0:29:46:b0:3d:f6:38:d7:07:40:47:0e:
+ a3:a5:70:1c:8b:80:c1:81:d1:35:cd:3d:93:20:c6:
+ 7c:10:a4:09:ed:41:12:2e:c3:66:e5:47:96:58:de:
+ 53:1b:d5:67:2c:1d:55:3b:c1:03:28:cf:5e:aa:33:
+ 2b:8c:e1
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Authority Key Identifier:
+ keyid:26:4F:F6:F9:E6:8B:B6:F7:59:CE:30:23:5C:90:2E:AE:7A:20:C4:DB
+
+ X509v3 Basic Constraints:
+ CA:FALSE
+ X509v3 Key Usage:
+ Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
+ X509v3 Subject Alternative Name:
+ DNS:localhost
+ Signature Algorithm: sha256WithRSAEncryption
+ b0:df:99:da:44:e1:22:c6:51:da:e1:b5:a9:fd:fe:82:d6:74:
+ 07:ad:d4:b4:f8:29:3e:57:7a:1b:98:36:4e:0a:23:68:f5:27:
+ c7:52:59:90:cd:94:23:08:83:6f:a4:af:14:a3:e3:ed:f2:13:
+ e4:17:f7:7c:27:45:bc:8c:9a:1d:f3:90:c6:b4:3e:e8:7a:c1:
+ 18:e4:8e:8c:28:ac:02:c7:d1:4c:e3:67:7a:13:69:ff:a4:74:
+ c4:82:d7:54:d3:cb:7b:4e:f9:25:36:90:33:43:f0:b8:a5:e6:
+ 7c:ea:3d:41:fe:51:3c:bc:d2:c6:4e:9c:dc:04:69:23:08:70:
+ bf:69:2a:bd:28:8c:3f:a1:f0:b0:88:87:a2:af:63:85:86:e3:
+ 07:2a:74:89:d0:69:b3:8c:7d:a5:db:ec:f2:5c:56:33:89:04:
+ c6:75:a9:a2:b8:c0:1b:b5:dd:0f:96:50:71:ad:39:36:39:13:
+ d0:80:f3:c8:50:db:d2:65:4d:56:75:9c:70:c2:d6:0c:6b:4a:
+ 6e:f7:f1:76:1b:82:16:13:eb:37:4f:05:fd:8f:06:89:15:d7:
+ 6d:a7:4e:43:bb:ee:b1:a8:c0:f4:cd:d7:1f:17:c3:3f:1a:79:
+ 8f:6e:46:a4:e5:1f:82:8d:60:6f:6c:a2:f4:9b:6e:59:85:48:
+ 73:ae:78:dd:c1:fa:81:1f:38:56:84:fc:31:98:af:a8:e4:bf:
+ 62:45:16:38:4a:5d:0e:6a:c4:bf:e1:9b:2b:c4:eb:dc:d4:85:
+ 82:0f:6c:31:54:1c:46:62:51:22:c3:0d:e4:ca:2e:c9:5f:f5:
+ 8c:7a:8c:c2:1d:f2:a8:f9:65:e6:ca:4e:6d:21:4e:55:07:6c:
+ 58:0d:fd:59:76:9c:65:7f:26:8f:8b:7b:01:70:5f:59:25:66:
+ a8:9b:0a:70:a1:d8:fd:61:26:7e:4d:5f:3c:28:74:2b:94:fb:
+ 2a:8e:35:51:77:5a:96:a9:9b:4e:18:b6:6d:0b:55:4e:2e:15:
+ ca:e7:cb:15:29:0e:b9:fd:23:56:a7:ad:dc:a1:b9:1b:1b:19:
+ 24:10:e3:a5:cb:69:2b:40:74:3c:3e:31:ac:a9:0d:17:6b:51:
+ 61:d4:5e:d1:98:b6:81:29:55:92:1f:00:8d:4c:72:d4:3a:0e:
+ fd:1f:30:73:04:b8:99:6f:27:57:9a:6c:2b:e1:fa:c2:d3:bf:
+ d3:d2:24:f3:5c:30:a3:25:d6:f5:18:91:13:d4:55:1e:33:89:
+ b7:99:27:a9:14:e4:d9:32:50:ba:56:2f:53:b7:a1:d7:d3:14:
+ 2f:e2:73:5a:d4:b2:94:73:14:ef:ac:6f:a1:c1:84:31:17:fd:
+ fa:f8:62:d3:eb:a5:8a:34
+```
+
+#### Let's create a PKCS12 file to be used for starting a TLS server
+
+```bash
+$ openssl pkcs12 -export -in server.pem -inkey server.key -name localhost -out server.p12
+Enter Export Password: pwdServerP12
+Verifying - Enter Export Password: pwdServerP12
+```
+
+The password of the PKCS12 file is `pwdServerP12`
+
+The `server.p12` file can now be used to start a TLS server.
+
+#### Now we'll generate the `client-truststore.p12` file that will have the server CA certificate.
+Since we don't need to add the key of the certificate (only required to sign, not to verify), we can import it directly with keytool.
+
+```bash
+$ keytool -import -trustcacerts -alias server-ca -keystore client-truststore.p12 -file ca.crt
+Enter keystore password: pwdClientWithServerCA
+Re-enter new password: pwdClientWithServerCA
+Owner: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Issuer: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Serial number: ed8bcadd4888ffac
+Valid from: Sat Sep 15 08:10:22 CEST 2018 until: Tue Sep 12 08:10:22 CEST 2028
+Certificate fingerprints:
+ MD5: 25:38:06:14:D0:B3:36:81:65:FC:44:CA:E3:BA:57:12
+ SHA1: 77:56:EF:C7:2F:5A:29:D1:A0:54:5F:F8:B4:19:60:91:7B:71:E4:2C
+ SHA256: 1D:2D:E5:52:21:60:75:08:F3:0A:B3:93:CF:38:F6:30:88:56:28:73:20:BA:76:9A:C0:A1:D7:8C:4D:D3:84:AA
+Signature algorithm name: SHA256withRSA
+Subject Public Key Algorithm: 4096-bit RSA key
+Version: 3
+
+Extensions:
+
+#1: ObjectId: 2.5.29.35 Criticality=false
+AuthorityKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68 BD D6 44 99 0E AD FC FC ...#...h..D.....
+0010: A5 31 90 D4 .1..
+]
+]
+
+#2: ObjectId: 2.5.29.19 Criticality=true
+BasicConstraints:[
+ CA:true
+ PathLen:2147483647
+]
+
+#3: ObjectId: 2.5.29.37 Criticality=false
+ExtendedKeyUsages [
+ serverAuth
+]
+
+#4: ObjectId: 2.5.29.15 Criticality=false
+KeyUsage [
+ DigitalSignature
+ Key_Encipherment
+ Data_Encipherment
+ Key_CertSign
+ Crl_Sign
+]
+
+#5: ObjectId: 2.5.29.14 Criticality=false
+SubjectKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68 BD D6 44 99 0E AD FC FC ...#...h..D.....
+0010: A5 31 90 D4 .1..
+]
+]
+
+Trust this certificate? [no]: yes
+Certificate was added to keystore
+```
+
+### Create a certificate that will be used to authenticate a user
+
+The principle is the same we'll have a CA authority signing certificates that will be sent by the user to the server.
+In this case the server will have to host the CA authority in its TrustedKeyStore while the client will host his certificate in is KeyStore.
+In this use case, the extensions are not the same, so we'll use openssl-client-auth.conf
+
+#### Generation of CA
+
+One line to generate both the key `ca-lient-auth.key` and the CA certificate `ca-client-auth.crt`
+
+```bash
+openssl req -newkey rsa:4096 -nodes -keyout ca-client-auth.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca-client-auth.crt -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=SonarSource/' -config ./openssl-client-auth.conf
+Generating a 4096 bit RSA private key
+...................................++
+............................................................................................................................................................................................................................................................++
+writing new private key to 'ca-client-auth.key'
+-----
+
+```
+
+For the certificate, the Common Name is used to identify the user
+```bash
+$ openssl req -new -keyout client.key -out client.csr -nodes -newkey rsa:4096 -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=Julien Henry/' -config ./openssl-client-auth.conf
+Generating a 4096 bit RSA private key
+..............................................++
+................++
+writing new private key to 'client.key'
+-----
+```
+
+Let's sign this certificate
+```bash
+$ openssl x509 -req -days 3650 -in client.csr -CA ca-client-auth.crt -CAkey ca-client-auth.key -CAcreateserial -out client.pem -sha256
+Signature ok
+subject=C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = Julien Henry
+Getting CA Private Key
+```
+
+Let's create the pkcs12 store containing the client certificate
+
+```bash
+$ openssl pkcs12 -export -in client.pem -inkey client.key -name julienhenry -out client.p12
+Enter Export Password: pwdClientCertP12
+Verifying - Enter Export Password: pwdClientCertP12
+```
+
+This will go to client keyStore.
+Now we'll generate the `server-with-client-ca.p12` file that will have the CA certificate. Since we don't need to add the key of the certificate (only required to sign, not to verify), we can import it directly with keytool.
+
+```bash
+$ keytool -import -trustcacerts -alias client-ca -keystore server-with-client-ca.p12 -file ca-client-auth.crt
+Enter keystore password: pwdServerWithClientCA
+Re-enter new password: pwdServerWithClientCA
+Owner: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Issuer: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Serial number: ed8bcadd4888ffac
+Valid from: Sat Sep 15 08:10:22 CEST 2018 until: Tue Sep 12 08:10:22 CEST 2028
+Certificate fingerprints:
+ MD5: 25:38:06:14:D0:B3:36:81:65:FC:44:CA:E3:BA:57:12
+ SHA1: 77:56:EF:C7:2F:5A:29:D1:A0:54:5F:F8:B4:19:60:91:7B:71:E4:2C
+ SHA256: 1D:2D:E5:52:21:60:75:08:F3:0A:B3:93:CF:38:F6:30:88:56:28:73:20:BA:76:9A:C0:A1:D7:8C:4D:D3:84:AA
+Signature algorithm name: SHA256withRSA
+Subject Public Key Algorithm: 4096-bit RSA key
+Version: 3
+
+Extensions:
+
+#1: ObjectId: 2.5.29.35 Criticality=false
+AuthorityKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68 BD D6 44 99 0E AD FC FC ...#...h..D.....
+0010: A5 31 90 D4 .1..
+]
+]
+
+#2: ObjectId: 2.5.29.19 Criticality=true
+BasicConstraints:[
+ CA:true
+ PathLen:2147483647
+]
+
+#3: ObjectId: 2.5.29.37 Criticality=false
+ExtendedKeyUsages [
+ serverAuth
+]
+
+#4: ObjectId: 2.5.29.15 Criticality=false
+KeyUsage [
+ DigitalSignature
+ Key_Encipherment
+ Data_Encipherment
+ Key_CertSign
+ Crl_Sign
+]
+
+#5: ObjectId: 2.5.29.14 Criticality=false
+SubjectKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68 BD D6 44 99 0E AD FC FC ...#...h..D.....
+0010: A5 31 90 D4 .1..
+]
+]
+
+Trust this certificate? [no]: yes
+Certificate was added to keystore
+
+``` \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt
new file mode 100644
index 00000000000..9b362ebb434
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key
new file mode 100644
index 00000000000..7ac7d7dda33
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+53j8dPRgcqZGjgO/EG0uAj3DINgNOg==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca.crt b/sonar-scanner-engine/src/test/resources/ssl/ca.crt
new file mode 100644
index 00000000000..8259e8378fa
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/ca.crt
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca.key b/sonar-scanner-engine/src/test/resources/ssl/ca.key
new file mode 100644
index 00000000000..cb3a442628c
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/ca.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
++iNXE0r4lx4TmqctoBApwtiahBnPfg==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12 b/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12
new file mode 100644
index 00000000000..21d251ec157
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.csr b/sonar-scanner-engine/src/test/resources/ssl/client.csr
new file mode 100644
index 00000000000..1dea36db6c7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/client.csr
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIE9TCCAt0CAQAwXzELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G
+A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTEVMBMGA1UEAwwM
+SnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1d3G
+hgrgkmtWl2rMZBMWTUIONuc7nDRG+DmvsOesTTx9AjCO0Wn3q+4Zj3LLQiq9j/N0
+goI4BrBluq4Nz6d08y0O5zOPJg9qCQpPlEWXSbrK39OMjRHJn+WmKnHF59EAo0Se
+8upR5yF1xBfmb/DGBm30eWnZDf9XKXDqZV+GgT/d/3+569kuJGJEcjuj+vrNM3z/
+WXSUS7mNAaDqUApqAZ4/kQa0fybGpC2MwfEzESS+R2RHyPQy1HVXsqkrTnxPSWZO
+DC20uWXDk1IlCCgqzKgkbjTvFMsn9GWwaYjcZQXe2b/pgWWMRc31hp6sjUO9EYOn
+Y7KLbVsJ558LlzK+8t45kpvPp2OHTPjs+ledSr+m0LAVFn6uXMOCudpo41yHggol
+Ep4OpxytkvDfozs9teHbh0NooWnwo65+h7YsEB9CGzG9tIeWZdu51KlPPAYpNafT
+9gUR4mDT4a20vGzHb3KtYmPQp6MQc/WwBajW7lE0jFTkukCNhBgL8csEZLgusYui
+36mnct3VMPaQXgGQJaR5//Zmp09aTwB9ILl7bzHAG3i5Um3OdNtTa3VW/N1U9lrT
+sNrRz2IfEWAwWFhK/oTcyhBx0kIfW5ju2cWbD76Yfq27uiHgYKTPJI/kN/8Vursx
+ex7rFKiU+hIvFbB61mqBN9OG7TKjWnJF8oa9GjkCAwEAAaBRME8GCSqGSIb3DQEJ
+DjFCMEAwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwEwYDVR0lBAwwCgYIKwYBBQUH
+AwIwEQYJYIZIAYb4QgEBBAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQATY8bk0rcI
+lwMw9FAYOTrQy4ZVuSEymby01ebjJ6I/1s+UQa6TI9pDigvomy+CgUWdXhquzQ8B
+dnOJGibTc3H2iMD5sb0+QhqhJtmCASup9ar1+JQTBEfcorVo2/8qkoEg5oDP+VEI
+oHiVzK5LRxfXwHiAoZURi/VJjp+0JJYaCBKrb8hVoGVStTG6qNKFi685v6KhVmM8
+Ng3JzuwC15e6LQ9Nr0xULaGcOZR0N/wkUl+2ntXDLIiVORyXmv4lY/Jp7bHhH7qr
+SecpEROzOZzIdzG4Iyy5Mj/DIPqKk55f5HuVXyhRiar89TBKrIwBVytlKPOZH4Ot
+rUTbKnz6OqBvXPFV6+YNyQb4LesU0CSWUVzFtYygBk1fcr+l7Bluxk29FTDf9iZL
+sc5v6fvrBhJmKHBQVTR1h9MvRpgpuImvRVdmOKNDXY7KpC7kB+DjKFEoQEtqJG0D
+53dxbkH7oaz57oSXPYjsAlezsDbAlWRFPD4gA+TqAJ0BkzoTggmctgJ8EpVkgzvG
+7iQAV+KN7OMClft4AQQO4224fSi9t8PSy+pWBc5wVDWLmnWxIkBHyBs+JgYJZ9pX
++Xo+S+FDxIK6ALirSeJlTDvIKgjgtLzXA6a+BNMNyQPKZHIjRqU36L/7fYyZnmTg
+aoBsmYBIm3xEMVi0VDBNFRPmLugPM2v9GQ==
+-----END CERTIFICATE REQUEST-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.key b/sonar-scanner-engine/src/test/resources/ssl/client.key
new file mode 100644
index 00000000000..42d414d4959
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/client.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+vsseryZb70+/NjZqWcSTcuFhooe9+Q==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.p12 b/sonar-scanner-engine/src/test/resources/ssl/client.p12
new file mode 100644
index 00000000000..d0a7a1fd2a8
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/client.p12
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.pem b/sonar-scanner-engine/src/test/resources/ssl/client.pem
new file mode 100644
index 00000000000..1b5e27ac3c3
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/client.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFRDCCAywCFF+7ThfA7f+eATq97zVNq8PRccLPMA0GCSqGSIb3DQEBCwUAMF4x
+CzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExDzANBgNVBAcMBkdlbmV2YTEX
+MBUGA1UECgwOU29uYXJTb3VyY2UgU0ExFDASBgNVBAMMC1NvbmFyU291cmNlMB4X
+DTI0MDQxNjA5MzYwM1oXDTM0MDQxNDA5MzYwM1owXzELMAkGA1UEBhMCQ0gxDzAN
+BgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNv
+dXJjZSBTQTEVMBMGA1UEAwwMSnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA1d3GhgrgkmtWl2rMZBMWTUIONuc7nDRG+DmvsOesTTx9
+AjCO0Wn3q+4Zj3LLQiq9j/N0goI4BrBluq4Nz6d08y0O5zOPJg9qCQpPlEWXSbrK
+39OMjRHJn+WmKnHF59EAo0Se8upR5yF1xBfmb/DGBm30eWnZDf9XKXDqZV+GgT/d
+/3+569kuJGJEcjuj+vrNM3z/WXSUS7mNAaDqUApqAZ4/kQa0fybGpC2MwfEzESS+
+R2RHyPQy1HVXsqkrTnxPSWZODC20uWXDk1IlCCgqzKgkbjTvFMsn9GWwaYjcZQXe
+2b/pgWWMRc31hp6sjUO9EYOnY7KLbVsJ558LlzK+8t45kpvPp2OHTPjs+ledSr+m
+0LAVFn6uXMOCudpo41yHggolEp4OpxytkvDfozs9teHbh0NooWnwo65+h7YsEB9C
+GzG9tIeWZdu51KlPPAYpNafT9gUR4mDT4a20vGzHb3KtYmPQp6MQc/WwBajW7lE0
+jFTkukCNhBgL8csEZLgusYui36mnct3VMPaQXgGQJaR5//Zmp09aTwB9ILl7bzHA
+G3i5Um3OdNtTa3VW/N1U9lrTsNrRz2IfEWAwWFhK/oTcyhBx0kIfW5ju2cWbD76Y
+fq27uiHgYKTPJI/kN/8Vursxex7rFKiU+hIvFbB61mqBN9OG7TKjWnJF8oa9GjkC
+AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAfmCoELV+5Nk4Cz0FP6xT2FP1YNXJJR2u
+qNilLDL6TqD/ONgPdmbMgwx5dNcSxOL8Fg2dUENgjwyl96WAfnFy+F6V9PFZmVk3
+BE0CWBMMb37Vr3gptyBdV9sOtgM4lIUwu+PYh4ykvk+cd6l7GfWBIDKpKA0mXJyY
+V3IkaFuxboOYE8CptNMH42vYPfAxQi+hFLzj97+Jhb/Ycv7gZHEvjt+EtSMmPTta
+OtOVMV0c8rFgvFraFloX0idEswBRzt+LG1F10zXQwG3aRLqde9t/SiQuTL1pctLt
+VMidqUSHfVYJOiqKnwZ+SWq75VRPBRSPowriZG2bOuLpkAYCEOU5+tYh3+pcGptq
+dl8Zcxrhmc9/LcDjPNtvxSNKQoM5rCVW2BRdqv0E0uxujlxtiWfQ3UnjfLdJgW1Y
+wF5kMSn6A7Nt+SkLMfao4Y1Toj/EywPvpnf8Dw4DwYd7UtKXKdY7i1mBAmqmSemN
+M2VLsP+O8+njHP+qtXPBsU8PBrgGpDh4zgmEGUoAPDea9uhX7F0ArSxZhOUpDGIj
+8HNBN52R+lZpjIZeIQlS2sIUGVm7XQY8I/11u26VgiKyPJxUOp19RjLzulk0v7yb
+MOGdW8OcQ03On9ucoTC2AbknqxX5/3fmH3f5ztbcGtZ0lE5LihySNyk7wLgkqHUA
+8JnfYM95xuk=
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf b/sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf
new file mode 100644
index 00000000000..a80df826a19
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf
@@ -0,0 +1,33 @@
+HOME = .
+RANDFILE = $ENV::HOME/.rnd
+
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+req_extensions = client_extensions
+
+[ req_distinguished_name ]
+countryName = Country Name (2-letter code)
+countryName_default = CH
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Geneva
+localityName = Locality (e.g. city name)
+localityName_default = Geneva
+organizationName = Organization (e.g. company name)
+organizationName_default = SonarSource SA
+commonName = Common Name (your.domain.com)
+commonName_default = Julien Henry
+
+[ client_extensions ]
+basicConstraints = CA:FALSE
+keyUsage = digitalSignature, keyEncipherment, dataEncipherment
+extendedKeyUsage = clientAuth
+nsCertType = client
+
+[ ca_extensions ]
+basicConstraints = CA:FALSE
+keyUsage = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature
+extendedKeyUsage = serverAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always, issuer
+basicConstraints = critical, CA:true \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/openssl.conf b/sonar-scanner-engine/src/test/resources/ssl/openssl.conf
new file mode 100644
index 00000000000..dc674dbbbe9
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/openssl.conf
@@ -0,0 +1,34 @@
+HOME = .
+
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+req_extensions = req_extensions
+
+[ req_distinguished_name ]
+countryName = Country Name (2-letter code)
+countryName_default = CH
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Geneva
+localityName = Locality (e.g. city name)
+localityName_default = Geneva
+organizationName = Organization (e.g. company name)
+organizationName_default = SonarSource SA
+commonName = Common Name (your.domain.com)
+commonName_default = localhost
+
+[ req_extensions ]
+subjectAltName = @alt_names
+keyUsage = keyEncipherment, dataEncipherment, digitalSignature
+extendedKeyUsage = serverAuth
+
+[ ca_extensions ]
+basicConstraints = CA:FALSE
+keyUsage = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature
+extendedKeyUsage = serverAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always, issuer
+basicConstraints = critical, CA:true
+
+[ alt_names ]
+DNS.1 = localhost \ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12 b/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12
new file mode 100644
index 00000000000..d95e0a9183f
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.csr b/sonar-scanner-engine/src/test/resources/ssl/server.csr
new file mode 100644
index 00000000000..a5028f04570
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/server.csr
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIE6jCCAtICAQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G
+A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJ
+bG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRRQF25E
+5NCgXdoEBU2SWyAoyOWMGVT1Ioltnr3sJP6LMjjfozK5YgaRn504291lwlG+k6tv
+zTSR9HB8q3ITa8AdnwMiL7jzbveYKWIlLQ7kdHKXWbiaIjTaZCyfnWUlDFIuR7BH
+wOXVwyLrBQfhoyDVaaoyowQEsUro3okIR/kBsqM+KH8bcdl06DMMppZ8Qy1DYvPo
+dhnNRyOSSpfbIoodE1fju+5U0OKzvGIc9WpG5pKIysaW3whOa/ieb02SXrgoiHnY
+PpmmGzm4u/Wn8jGwhYQJSQT10yjMacGHwmBEq7FUr854cVd+eend056P6pwUukdN
+eVHCFjYRkmWCNzIxV+sS9PPtDs77/bLFIItrnBMHVsId38tPoru/z1S1p2dzCX3N
+q09aJFF/vH2u9Sg5aerHJ7xnRroR1jIrAZtcjBkJHEiTlG+WaavP4j6oym+lvHvg
+HHL3Qwhh8emg0JiLYExVV7ma70aRDh8yoQtSzAUDMVfhVPKd92MS+7DC2pv2KviU
+NKqbHDFadl01JN3t+17/gstUNSk1jpoUfUhKBeUQxVEdVUy2p0HeD/TYpRvF2FEs
+Wneq3+ZbnRp17I/uEQOck0LP2tkzAd4tmRgH+95yyB8MgbAfvyKWkB4+3BhtdfoY
+De1asqR6z43mejDHHqgBXn+u3UKjPypKfPECAwEAAaBJMEcGCSqGSIb3DQEJDjE6
+MDgwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAsGA1UdDwQEAwIEsDATBgNVHSUEDDAK
+BggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAgEAWLCYfUyCFT1bOafpcrjdpHaE
+Z+DgrFniECq/gbx3DYdYHCxmfXVKuLGby2uNoujPPW4pIRzfbFjijoHN50uF8tnX
+qtpHlEACoFnpMUOfTyP9FnjT33VdSNoMdXv1xfRtIZE+WUE9CemYFBChGobo861a
+au3QYyyHiSTeAsE/cNOBoaNwvUieTRjX8NAq69vkkmFaEIstKingEt3HVMD3yZPu
+qIMRuzrH63H0YOf9BsHieRTbV4h8FzmJyuaHMGpmBbP5zM3ynvoBlCki3sLYQQ78
+jrfxLHpco40Re4/Er9qra5LtEYBZEaooTHd1oPxyIxGiaw6pLLi/VlWy64L9Bt9w
+Ta2suIHoUElL7F+QXkh6+16ljdghjJCvhIrFOdf7hzcG+x3u4H24Nm5bxYXiSpQD
+DAkj1mzuO49iqRhytEh6LMAXtMp8ntX79gYLkyaa0TRChYqwYNNlTzAmsG4vO7Hy
++dOc1hF5RIQd3TL7jmGDLlI+Y4DXrPMVa/6edimIcBFCgdYau70vBK9P5z+5HroL
+AZS2/kxTT3eaWBF77mCfyqXq/qfiUaQk1d7qszfJxpdbar4tJqOPXxJa8mK1AIuI
+tezgiHDB+4mM9XhVF2tdIekLfvVuRIKOA+9xbPtnrYRngH0VLcPOYlt+eC95N45D
+cMyWA9ULzIcwEeBjYSE=
+-----END CERTIFICATE REQUEST-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.key b/sonar-scanner-engine/src/test/resources/ssl/server.key
new file mode 100644
index 00000000000..c0568c3eb4d
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/server.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCtFFAXbkTk0KBd
+2gQFTZJbICjI5YwZVPUiiW2evewk/osyON+jMrliBpGfnTjb3WXCUb6Tq2/NNJH0
+cHyrchNrwB2fAyIvuPNu95gpYiUtDuR0cpdZuJoiNNpkLJ+dZSUMUi5HsEfA5dXD
+IusFB+GjINVpqjKjBASxSujeiQhH+QGyoz4ofxtx2XToMwymlnxDLUNi8+h2Gc1H
+I5JKl9siih0TV+O77lTQ4rO8Yhz1akbmkojKxpbfCE5r+J5vTZJeuCiIedg+maYb
+Obi79afyMbCFhAlJBPXTKMxpwYfCYESrsVSvznhxV3556d3Tno/qnBS6R015UcIW
+NhGSZYI3MjFX6xL08+0Ozvv9ssUgi2ucEwdWwh3fy0+iu7/PVLWnZ3MJfc2rT1ok
+UX+8fa71KDlp6scnvGdGuhHWMisBm1yMGQkcSJOUb5Zpq8/iPqjKb6W8e+AccvdD
+CGHx6aDQmItgTFVXuZrvRpEOHzKhC1LMBQMxV+FU8p33YxL7sMLam/Yq+JQ0qpsc
+MVp2XTUk3e37Xv+Cy1Q1KTWOmhR9SEoF5RDFUR1VTLanQd4P9NilG8XYUSxad6rf
+5ludGnXsj+4RA5yTQs/a2TMB3i2ZGAf73nLIHwyBsB+/IpaQHj7cGG11+hgN7Vqy
+pHrPjeZ6MMceqAFef67dQqM/Kkp88QIDAQABAoICACd11N/OsG7hoNpc6SNDYQ2d
+GqdY7HTfEYeHASLaxrLVhOtVm6lD2I/AkyVqrUq1YqynwfU9dh85L9ik58uX1dUw
+ZyB4kKwENR4U3ZB706F+/neNI7QdOij3113U7awvIf/54ZrPFkDktbSIasBKIHe2
+demiF+riMOax/z8zS1vLagduH+8QMbPmgfipoOX/M8QGFxHBrbt1XP+t3L3ceuXY
+StI87MtNRnGcaiGWVedfDFynxn/CwKWHaYfE1mxmaWtmfblF3FdDZSNaaOOTma+G
+hCogpRRMiPZUXCx1ZuwaUjW516bAgmXG7qtBdmV3xnSVEsW4mXGCQieZuq5frczi
+3+dRbYC4VkgmzW63XrgH0FuNFJ4sMdB8QeYRl3FDP+wMhL2ObvABmpvReASWiVON
+R+x9ssYLQUdS/Znr8M8KWDvZ+zURyzbz9kdkPJseV/Xeekt76nbNRd0jsmjt6n1w
+eQa2hFocN7CgM1pg8EIW/xLNbkokYtAZAGnHgTuLlFMJSgMj0u8+NyZsIHrbW36X
+7XBWaMM145G3PpnycoYZ3UAyYJTGPFEN5FImoZwJAIS2lKpoQN2G5T+iPeFQDmvX
+pO/Y21tj2wjl6y1mT+GvurnF980+aS2Ibu0cN8BTxpPAt1f/oOUKKv4cPYGfsXq4
+TKtoqPkPGhSSIyuyfHKlAoIBAQC/jHbybKXmodEc7S9AS5p+O7WTT0jQO6VMK/cp
+woEcZlnDDqowkiaX+Wn7ybFDAkZwioCtLnRx8IeMmDLJpfRyme/wNEDY62pEncHY
+MZwEEUT1GtBYNAmkKLWO+ThfGiUGHnMw2F1nXbZRzNZLpt7U6/of3ydUVcWDwMus
+29gLbRgPQ2DMdrPLsUk3T82TaNv0pdCvfZjDW08rjS4/mBgiIdnXT4q5P2wBTmFz
+1tfXMtXp5puw3qYbPGiX3PYozIWMXzZnmbgCijUCNrBxbxA9YKjElkIfTur3y/W0
+Z7Li2pROQyaQ6gGixf876LAl27xCRpGzoBlTBYMnlciI1Yc9AoIBAQDnUPG4zTLZ
+YVoBt5tP7lXRE8LsQUC30LK6Rszu3TRnzvQP7qYVo7/RC7J3DhyghSEFEwdQ27Vb
+9buAkowVwhWf6rpygwUChaiT7xOK9NaIvCybBSa9ZpPVe07mAuVNYzp2qE6DXrW3
+/gvm5twr7Noc/YS210xa2SB5Gun7TghaAb/mjGDZSa0JnqVDz3BOo3YmBdgHcAic
+fsRRmK3YmYDraxSZuD+cs30jPZw60c7RSdN59kf7i8cQ6G2gZCVo1hVPDys95jbN
+jtlu1OOdLv83iMEw/W2rpMOCMy5+Dhielx5Ra/K2T6OZC8woXcza8JFy9xDiF6DC
+Okqm7rZOzcfFAoIBAQCYuogRDd5OAZI5zUiSrHWX10YVGe+F0TkgfiHKE0NdAKLr
+q2K57Z6GKKF/2LbVJhhCHb0x2MuSGeYKjURZklBRnDo7PX7DNxn5cgwgtJWgjKB+
+Co469esGEEuLn116Pt9sfJT+SlZXV9pKaNgpY/lirnE2PnkefnFJd00vG++sVKUN
+bnzdKnx7mnU1fBT/R2myLRAzDSLkCYcbw6svm7cKaBFI4yxKPq6AcB21/oUFGoyD
+vpM/OJgbOVRwWgeQSlrlrPk1K9UTeV2A0VhoadT6C3slnGVGj2c8g0z7Nn/k78G2
+kUZL37nELrku7H6fARCfi6MbJTlsAAYuZviJWjBFAoIBAFTkjBH6nRLSe6ntrH5l
+RfF5gywZtpq/aRicK1HutPD0LvY565I9ioQ5+sFe2HrA4SFvnlu6hpC9WpcRMYA6
+vpz2FH86PnhyfS/tqgpxWNrN1MD/3vvbzZ2np4kavvTr2eT6V/Y2qBJilhOj3mHw
+hwvkrvQ7h7Y/wX8wtXaZaM8/nSILmu+j7nF9W8HLO7hgnVfPBT2VjFOC4qHfms3H
+aFz9642O5SmpZd+tGM0teu2sXoSAMmLLJb+6zaDzoBcdmqxtML2C49IE/x+B2hcx
+zFChS+Wi3MEFswrxpbp1ieuKIoJXT7hA+hWNEtwtsKUZbQf4TKXtbf5aTlN9gELj
+mtECggEAC0x8yvZefPbczQ10qENLmnfYrSGjiuXN794Z3y4DwO8R0xnfdYy8zibQ
+QF3+3LqhKVtu7DVF1ZJvisl+WntURA8oUp8VUeMwLymf+LBSnXfD7FhtWxkk/v7M
+KqHPUFzIi/fxKUoiVxgS2QDjCSU9zIzDFD/Uz4KMDq70wmRPf621tSfxFMEQ//Cd
+DjRhrqu1Vs8Gf29QXjOfrz9DnRI4IzXRr0+uKEd4H8caMK3uGFFEUnDBU7amw5Tz
+SVf9+1Uhseo+hLJlproKDB6TIkx06sv7bMpfpZiMEkT2zzQh59WFLeX57FIeUiBu
+9uPzaGE09HfpBsTJtUTjU0mClVlOCw==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.p12 b/sonar-scanner-engine/src/test/resources/ssl/server.p12
new file mode 100644
index 00000000000..17a81a1dff5
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/server.p12
Binary files differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.pem b/sonar-scanner-engine/src/test/resources/ssl/server.pem
new file mode 100644
index 00000000000..61792208fb7
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/server.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtjCCA56gAwIBAgIULroxFuPWyNOiQtAVPS/XFFMXp6owDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu
+ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0
+MB4XDTI0MDQxNjA4NDUyMVoXDTM0MDQxNDA4NDUyMVowXDELMAkGA1UEBhMCQ0gx
+DzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25h
+clNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEArRRQF25E5NCgXdoEBU2SWyAoyOWMGVT1Ioltnr3sJP6L
+MjjfozK5YgaRn504291lwlG+k6tvzTSR9HB8q3ITa8AdnwMiL7jzbveYKWIlLQ7k
+dHKXWbiaIjTaZCyfnWUlDFIuR7BHwOXVwyLrBQfhoyDVaaoyowQEsUro3okIR/kB
+sqM+KH8bcdl06DMMppZ8Qy1DYvPodhnNRyOSSpfbIoodE1fju+5U0OKzvGIc9WpG
+5pKIysaW3whOa/ieb02SXrgoiHnYPpmmGzm4u/Wn8jGwhYQJSQT10yjMacGHwmBE
+q7FUr854cVd+eend056P6pwUukdNeVHCFjYRkmWCNzIxV+sS9PPtDs77/bLFIItr
+nBMHVsId38tPoru/z1S1p2dzCX3Nq09aJFF/vH2u9Sg5aerHJ7xnRroR1jIrAZtc
+jBkJHEiTlG+WaavP4j6oym+lvHvgHHL3Qwhh8emg0JiLYExVV7ma70aRDh8yoQtS
+zAUDMVfhVPKd92MS+7DC2pv2KviUNKqbHDFadl01JN3t+17/gstUNSk1jpoUfUhK
+BeUQxVEdVUy2p0HeD/TYpRvF2FEsWneq3+ZbnRp17I/uEQOck0LP2tkzAd4tmRgH
++95yyB8MgbAfvyKWkB4+3BhtdfoYDe1asqR6z43mejDHHqgBXn+u3UKjPypKfPEC
+AwEAAaNwMG4wHwYDVR0jBBgwFoAUINXfg3fn6/RUenW3EobpMoP8wDQwCQYDVR0T
+BAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQW
+BBRX4bsny+8GQcFpM10jtAfFxzNxzzANBgkqhkiG9w0BAQsFAAOCAgEAa+Myw6li
+Fme95cPpINTite/9LXk+TlHHnXiV5Z+Um3NTLSllX3zPuRFiOE71OKFrWQPqH2N/
+85l6h19G9xQsaqkkVFyQENkNzykZpJL/jU4+wgRtwcEDkaRGGURZacz3vfLTc1HX
+tPDNv/JsZ5HE2d7cF5YhN4UahtxS2lvarrSujaOBpFZTT6PbEYX9EnwCdapORHOh
+wKMc3OGGOiGWvRlVaWu/Huq2HvXXcK0pmaYWWKX3u21evthSYOu9U4Rk0z1y7m3/
+CIYaIrvSbkzq2KKXMn7lr26bv2cthAQrPAjb2ILPUoyzKa3wEK3lkhanM6PN9CMH
+y5KRTpqwV45Qr6BAVY1bP67pEkay2T31chIVKds6dkx9b2/bWpW9PWuymsbWX2vO
+Q1MiaPkXKSTgCRwQUR0SNbPHw3X+VhrKKJB+beX8Bh2fcKw3jGGM8oHiA1hpdnbg
+Y5fW7EupF5gabf2jNB1XJ4gowlpB3nTooKFgbcgsvi68MRdBno2TWUhsZ3zCVyaH
+KFdDV0f78Fg7oL79K3kBL/iqr+jsb8sFHKIS4Dyyz2rDJrE0q0xAPes+Bu75R3/5
+M/s2H7KuLqLdDYsCsMeMqOVuIcAyPp2MFWInYPyi0zY4fwKwm8f/Kv8Lzb+moxqI
+Fct6d1S08JAosVnZcP2P7Yz+TbmDRtsqCgk=
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/v3.ext b/sonar-scanner-engine/src/test/resources/ssl/v3.ext
new file mode 100644
index 00000000000..8027d8f19cf
--- /dev/null
+++ b/sonar-scanner-engine/src/test/resources/ssl/v3.ext
@@ -0,0 +1,7 @@
+authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = localhost \ No newline at end of file