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'
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'
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'
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'
+++ /dev/null
-/*
- * 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.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.CheckForNull;
-import org.apache.commons.lang3.StringUtils;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.notifications.AnalysisWarnings;
-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.sonarqube.ws.client.HttpException;
-import org.sonarqube.ws.client.WsClient;
-import org.sonarqube.ws.client.WsConnector;
-import org.sonarqube.ws.client.WsRequest;
-import org.sonarqube.ws.client.WsResponse;
-
-import static java.lang.String.format;
-import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
-import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
-import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
-import static org.sonar.api.utils.Preconditions.checkState;
-
-public class DefaultScannerWsClient implements ScannerWsClient {
- private static final int MAX_ERROR_MSG_LEN = 128;
- private static final String SQ_TOKEN_EXPIRATION_HEADER = "SonarQube-Authentication-Token-Expiration";
- private static final DateTimeFormatter USER_FRIENDLY_DATETIME_FORMAT = DateTimeFormatter.ofPattern("MMMM dd, yyyy");
- private static final Logger LOG = Loggers.get(DefaultScannerWsClient.class);
-
- private final Set<String> warningMessages = new HashSet<>();
-
- private final WsClient target;
- private final boolean hasCredentials;
- private final GlobalAnalysisMode globalMode;
- private final AnalysisWarnings analysisWarnings;
-
- public DefaultScannerWsClient(WsClient target, boolean hasCredentials, GlobalAnalysisMode globalMode, AnalysisWarnings analysisWarnings) {
- this.target = target;
- this.hasCredentials = hasCredentials;
- this.globalMode = globalMode;
- this.analysisWarnings = analysisWarnings;
- }
-
- /**
- * If an exception is not thrown, the response needs to be closed by either calling close() directly, or closing the
- * body content's stream/reader.
- *
- * @throws IllegalStateException if the request could not be executed due to a connectivity problem or timeout. Because networks can
- * fail during an exchange, it is possible that the remote server accepted the request before the failure
- * @throws MessageException if there was a problem with authentication or if a error message was parsed from the response.
- * @throws HttpException if the response code is not in range [200..300). Consider using {@link #createErrorMessage(HttpException)} to create more relevant messages for the users.
- */
- public WsResponse call(WsRequest request) {
- checkState(!globalMode.isMediumTest(), "No WS call should be made in medium test mode");
- Profiler profiler = Profiler.createIfDebug(LOG).start();
- WsResponse response = target.wsConnector().call(request);
- profiler.stopDebug(format("%s %d %s", request.getMethod(), response.code(), response.requestUrl()));
- failIfUnauthorized(response);
- checkAuthenticationWarnings(response);
- return response;
- }
-
- public String baseUrl() {
- return target.wsConnector().baseUrl();
- }
-
- WsConnector wsConnector() {
- return target.wsConnector();
- }
-
- private void failIfUnauthorized(WsResponse response) {
- int code = response.code();
-
- if (code == HTTP_UNAUTHORIZED) {
- logResponseDetailsIfDebug(response);
- response.close();
- if (hasCredentials) {
- // credentials are not valid
- throw MessageException.of(format("Not authorized. Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
- ScannerWsClientProvider.TOKEN_PROPERTY, CoreProperties.LOGIN, CoreProperties.PASSWORD));
- }
- // not authenticated - see https://jira.sonarsource.com/browse/SONAR-4048
- throw MessageException.of(format("Not authorized. Analyzing this project requires authentication. " +
- "Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
- ScannerWsClientProvider.TOKEN_PROPERTY, CoreProperties.LOGIN, CoreProperties.PASSWORD));
- }
- if (code == HTTP_FORBIDDEN) {
- logResponseDetailsIfDebug(response);
- throw MessageException.of("You're not authorized to analyze this project or the project doesn't exist on SonarQube" +
- " and you're not authorized to create it. Please contact an administrator.");
- }
- if (code == HTTP_BAD_REQUEST) {
- String jsonMsg = tryParseAsJsonError(response.content());
- if (jsonMsg != null) {
- throw MessageException.of(jsonMsg);
- }
- }
- // if failed, throws an HttpException
- response.failIfNotSuccessful();
- }
-
- private static void logResponseDetailsIfDebug(WsResponse response) {
- if (!LOG.isDebugEnabled()) {
- return;
- }
- String content = response.hasContent() ? response.content() : "<no content>";
- Map<String, List<String>> headers = response.headers();
- LOG.debug("Error response content: {}, headers: {}", content, headers);
- }
-
- private void checkAuthenticationWarnings(WsResponse response) {
- if (response.code() == HTTP_OK) {
- response.header(SQ_TOKEN_EXPIRATION_HEADER).ifPresent(expirationDate -> {
- var datetimeInUTC = ZonedDateTime.from(DateTimeFormatter.ofPattern(DATETIME_FORMAT)
- .parse(expirationDate)).withZoneSameInstant(ZoneOffset.UTC);
- if (isTokenExpiringInOneWeek(datetimeInUTC)) {
- addAnalysisWarning(datetimeInUTC);
- }
- });
- }
- }
-
- private static boolean isTokenExpiringInOneWeek(ZonedDateTime expirationDate) {
- ZonedDateTime localDateTime = ZonedDateTime.now(ZoneOffset.UTC);
- ZonedDateTime headerDateTime = expirationDate.minusDays(7);
- return localDateTime.isAfter(headerDateTime);
- }
-
- private void addAnalysisWarning(ZonedDateTime tokenExpirationDate) {
- String warningMessage = "The token used for this analysis will expire on: " + tokenExpirationDate.format(USER_FRIENDLY_DATETIME_FORMAT);
- if (!warningMessages.contains(warningMessage)) {
- warningMessages.add(warningMessage);
- LOG.warn(warningMessage);
- LOG.warn("Analysis executed with this token will fail after the expiration date.");
- }
- analysisWarnings.addUnique(warningMessage + "\nAfter this date, the token can no longer be used to execute the analysis. "
- + "Please consider generating a new token and updating it in the locations where it is in use.");
- }
-
- /**
- * Tries to form a short and relevant error message from the exception, to be displayed in the console.
- */
- public static String createErrorMessage(HttpException exception) {
- String json = tryParseAsJsonError(exception.content());
- if (json != null) {
- return json;
- }
-
- String msg = "HTTP code " + exception.code();
- if (isHtml(exception.content())) {
- return msg;
- }
-
- return msg + ": " + StringUtils.left(exception.content(), MAX_ERROR_MSG_LEN);
- }
-
- @CheckForNull
- private static String tryParseAsJsonError(String responseContent) {
- try {
- JsonObject obj = JsonParser.parseString(responseContent).getAsJsonObject();
- JsonArray errors = obj.getAsJsonArray("errors");
- List<String> errorMessages = new ArrayList<>();
- for (JsonElement e : errors) {
- errorMessages.add(e.getAsJsonObject().get("msg").getAsString());
- }
- return String.join(", ", errorMessages);
- } catch (Exception e) {
- return null;
- }
- }
-
- private static boolean isHtml(String responseContent) {
- return StringUtils.stripToEmpty(responseContent).startsWith("<!DOCTYPE html>");
- }
-}
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;
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 {
} 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);
}
}
}
- 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());
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;
}
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;
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);
}
*/
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));
}
// 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);
}
}
- 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();
- }
}
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;
+++ /dev/null
-/*
- * 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 org.sonarqube.ws.client.WsRequest;
-import org.sonarqube.ws.client.WsResponse;
-
-public interface ScannerWsClient {
- WsResponse call(WsRequest request);
-
- String baseUrl();
-
-}
+++ /dev/null
-/*
- * 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.net.InetSocketAddress;
-import java.net.Proxy;
-import java.time.Duration;
-import java.time.format.DateTimeParseException;
-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.sonarqube.ws.client.HttpConnector;
-import org.sonarqube.ws.client.WsClientFactories;
-import org.springframework.context.annotation.Bean;
-
-import static java.lang.Integer.parseInt;
-import static java.lang.String.valueOf;
-import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
-import static org.apache.commons.lang3.StringUtils.isNotBlank;
-import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_PASSWORD;
-import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_USER;
-
-public class ScannerWsClientProvider {
- static final int DEFAULT_CONNECT_TIMEOUT = 5;
- static final int DEFAULT_RESPONSE_TIMEOUT = 0;
- static final String READ_TIMEOUT_SEC_PROPERTY = "sonar.ws.timeout";
- public static final String TOKEN_PROPERTY = "sonar.token";
- private static final String TOKEN_ENV_VARIABLE = "SONAR_TOKEN";
- static final int DEFAULT_READ_TIMEOUT_SEC = 60;
- public static final String SONAR_SCANNER_PROXY_PORT = "sonar.scanner.proxyPort";
- public static final String SONAR_SCANNER_CONNECT_TIMEOUT = "sonar.scanner.connectTimeout";
- public static final String SONAR_SCANNER_SOCKET_TIMEOUT = "sonar.scanner.socketTimeout";
- public static final String SONAR_SCANNER_RESPONSE_TIMEOUT = "sonar.scanner.responseTimeout";
-
- @Bean("DefaultScannerWsClient")
- public DefaultScannerWsClient provide(ScannerProperties scannerProps, EnvironmentInformation env, GlobalAnalysisMode globalMode,
- System2 system, AnalysisWarnings analysisWarnings) {
- String url = defaultIfBlank(scannerProps.property("sonar.host.url"), "http://localhost:9000");
- HttpConnector.Builder connectorBuilder = HttpConnector.newBuilder().acceptGzip(true);
-
- String oldSocketTimeout = defaultIfBlank(scannerProps.property(READ_TIMEOUT_SEC_PROPERTY), valueOf(DEFAULT_READ_TIMEOUT_SEC));
- String socketTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_SOCKET_TIMEOUT), oldSocketTimeout);
- String connectTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_CONNECT_TIMEOUT), valueOf(DEFAULT_CONNECT_TIMEOUT));
- String responseTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_RESPONSE_TIMEOUT), valueOf(DEFAULT_RESPONSE_TIMEOUT));
- 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);
- 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));
-
- // OkHttp detects 'http.proxyHost' java property already, so just focus on sonar properties
- String proxyHost = defaultIfBlank(scannerProps.property("sonar.scanner.proxyHost"), null);
- if (proxyHost != null) {
- String proxyPortStr = defaultIfBlank(scannerProps.property(SONAR_SCANNER_PROXY_PORT), url.startsWith("https") ? "443" : "80");
- var proxyPort = parseIntProperty(proxyPortStr, SONAR_SCANNER_PROXY_PORT);
- connectorBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
- }
-
- var scannerProxyUser = scannerProps.property("sonar.scanner.proxyUser");
- String proxyUser = scannerProxyUser != null ? scannerProxyUser : system.properties().getProperty(HTTP_PROXY_USER, "");
- if (isNotBlank(proxyUser)) {
- var scannerProxyPwd = scannerProps.property("sonar.scanner.proxyPassword");
- String proxyPassword = scannerProxyPwd != null ? scannerProxyPwd : system.properties().getProperty(HTTP_PROXY_PASSWORD, "");
- connectorBuilder.proxyCredentials(proxyUser, proxyPassword);
- }
-
- return new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connectorBuilder.build()), login != null, globalMode, analysisWarnings);
- }
-
- private static int parseIntProperty(String propValue, String propKey) {
- try {
- return parseInt(propValue);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException(propKey + " is not a valid integer: " + propValue, e);
- }
- }
-
- /**
- * For testing, we can accept timeouts that are smaller than a second, expressed using ISO-8601 format for durations.
- * If we can't parse as ISO-8601, then fallback to the official format that is simply the number of seconds
- */
- private static int parseDurationProperty(String propValue, String propKey) {
- try {
- return (int) Duration.parse(propValue).toMillis();
- } catch (DateTimeParseException e) {
- return parseIntProperty(propValue, propKey) * 1_000;
- }
- }
-}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
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;
DefaultServer.class,
DefaultDocumentationLinkGenerator.class,
new GlobalTempFolderProvider(),
+ new SonarUserHomeProvider(),
analysisWarnings,
UriReader.class,
PluginFiles.class,
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;
--- /dev/null
+/*
+ * 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.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import org.apache.commons.lang3.StringUtils;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.notifications.AnalysisWarnings;
+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;
+import org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static java.lang.String.format;
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
+import static org.sonar.api.utils.Preconditions.checkState;
+
+public class DefaultScannerWsClient implements ScannerWsClient {
+ private static final int MAX_ERROR_MSG_LEN = 128;
+ private static final String SQ_TOKEN_EXPIRATION_HEADER = "SonarQube-Authentication-Token-Expiration";
+ private static final DateTimeFormatter USER_FRIENDLY_DATETIME_FORMAT = DateTimeFormatter.ofPattern("MMMM dd, yyyy");
+ private static final Logger LOG = Loggers.get(DefaultScannerWsClient.class);
+
+ private final Set<String> warningMessages = new HashSet<>();
+
+ private final WsClient target;
+ private final boolean hasCredentials;
+ private final GlobalAnalysisMode globalMode;
+ private final AnalysisWarnings analysisWarnings;
+
+ public DefaultScannerWsClient(WsClient target, boolean hasCredentials, GlobalAnalysisMode globalMode, AnalysisWarnings analysisWarnings) {
+ this.target = target;
+ this.hasCredentials = hasCredentials;
+ this.globalMode = globalMode;
+ this.analysisWarnings = analysisWarnings;
+ }
+
+ /**
+ * If an exception is not thrown, the response needs to be closed by either calling close() directly, or closing the
+ * body content's stream/reader.
+ *
+ * @throws IllegalStateException if the request could not be executed due to a connectivity problem or timeout. Because networks can
+ * fail during an exchange, it is possible that the remote server accepted the request before the failure
+ * @throws MessageException if there was a problem with authentication or if a error message was parsed from the response.
+ * @throws HttpException if the response code is not in range [200..300). Consider using {@link #createErrorMessage(HttpException)} to create more relevant messages for the users.
+ */
+ public WsResponse call(WsRequest request) {
+ checkState(!globalMode.isMediumTest(), "No WS call should be made in medium test mode");
+ Profiler profiler = Profiler.createIfDebug(LOG).start();
+ WsResponse response = target.wsConnector().call(request);
+ profiler.stopDebug(format("%s %d %s", request.getMethod(), response.code(), response.requestUrl()));
+ failIfUnauthorized(response);
+ checkAuthenticationWarnings(response);
+ return response;
+ }
+
+ public String baseUrl() {
+ return target.wsConnector().baseUrl();
+ }
+
+ WsConnector wsConnector() {
+ return target.wsConnector();
+ }
+
+ private void failIfUnauthorized(WsResponse response) {
+ int code = response.code();
+
+ if (code == HTTP_UNAUTHORIZED) {
+ logResponseDetailsIfDebug(response);
+ response.close();
+ if (hasCredentials) {
+ // credentials are not valid
+ throw MessageException.of(format("Not authorized. Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
+ ScannerWsClientProvider.TOKEN_PROPERTY, CoreProperties.LOGIN, CoreProperties.PASSWORD));
+ }
+ // not authenticated - see https://jira.sonarsource.com/browse/SONAR-4048
+ throw MessageException.of(format("Not authorized. Analyzing this project requires authentication. " +
+ "Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
+ ScannerWsClientProvider.TOKEN_PROPERTY, CoreProperties.LOGIN, CoreProperties.PASSWORD));
+ }
+ if (code == HTTP_FORBIDDEN) {
+ logResponseDetailsIfDebug(response);
+ throw MessageException.of("You're not authorized to analyze this project or the project doesn't exist on SonarQube" +
+ " and you're not authorized to create it. Please contact an administrator.");
+ }
+ if (code == HTTP_BAD_REQUEST) {
+ String jsonMsg = tryParseAsJsonError(response.content());
+ if (jsonMsg != null) {
+ throw MessageException.of(jsonMsg);
+ }
+ }
+ // if failed, throws an HttpException
+ response.failIfNotSuccessful();
+ }
+
+ private static void logResponseDetailsIfDebug(WsResponse response) {
+ if (!LOG.isDebugEnabled()) {
+ return;
+ }
+ String content = response.hasContent() ? response.content() : "<no content>";
+ Map<String, List<String>> headers = response.headers();
+ LOG.debug("Error response content: {}, headers: {}", content, headers);
+ }
+
+ private void checkAuthenticationWarnings(WsResponse response) {
+ if (response.code() == HTTP_OK) {
+ response.header(SQ_TOKEN_EXPIRATION_HEADER).ifPresent(expirationDate -> {
+ var datetimeInUTC = ZonedDateTime.from(DateTimeFormatter.ofPattern(DATETIME_FORMAT)
+ .parse(expirationDate)).withZoneSameInstant(ZoneOffset.UTC);
+ if (isTokenExpiringInOneWeek(datetimeInUTC)) {
+ addAnalysisWarning(datetimeInUTC);
+ }
+ });
+ }
+ }
+
+ private static boolean isTokenExpiringInOneWeek(ZonedDateTime expirationDate) {
+ ZonedDateTime localDateTime = ZonedDateTime.now(ZoneOffset.UTC);
+ ZonedDateTime headerDateTime = expirationDate.minusDays(7);
+ return localDateTime.isAfter(headerDateTime);
+ }
+
+ private void addAnalysisWarning(ZonedDateTime tokenExpirationDate) {
+ String warningMessage = "The token used for this analysis will expire on: " + tokenExpirationDate.format(USER_FRIENDLY_DATETIME_FORMAT);
+ if (!warningMessages.contains(warningMessage)) {
+ warningMessages.add(warningMessage);
+ LOG.warn(warningMessage);
+ LOG.warn("Analysis executed with this token will fail after the expiration date.");
+ }
+ analysisWarnings.addUnique(warningMessage + "\nAfter this date, the token can no longer be used to execute the analysis. "
+ + "Please consider generating a new token and updating it in the locations where it is in use.");
+ }
+
+ /**
+ * Tries to form a short and relevant error message from the exception, to be displayed in the console.
+ */
+ public static String createErrorMessage(HttpException exception) {
+ String json = tryParseAsJsonError(exception.content());
+ if (json != null) {
+ return json;
+ }
+
+ String msg = "HTTP code " + exception.code();
+ if (isHtml(exception.content())) {
+ return msg;
+ }
+
+ return msg + ": " + StringUtils.left(exception.content(), MAX_ERROR_MSG_LEN);
+ }
+
+ @CheckForNull
+ private static String tryParseAsJsonError(String responseContent) {
+ try {
+ JsonObject obj = JsonParser.parseString(responseContent).getAsJsonObject();
+ JsonArray errors = obj.getAsJsonArray("errors");
+ List<String> errorMessages = new ArrayList<>();
+ for (JsonElement e : errors) {
+ errorMessages.add(e.getAsJsonObject().get("msg").getAsString());
+ }
+ return String.join(", ", errorMessages);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static boolean isHtml(String responseContent) {
+ return StringUtils.stripToEmpty(responseContent).startsWith("<!DOCTYPE html>");
+ }
+}
--- /dev/null
+/*
+ * 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 org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+public interface ScannerWsClient {
+ WsResponse call(WsRequest request);
+
+ String baseUrl();
+
+}
--- /dev/null
+/*
+ * 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 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;
+
+import static java.lang.Integer.parseInt;
+import static java.lang.String.valueOf;
+import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_PASSWORD;
+import static org.sonar.core.config.ProxyProperties.HTTP_PROXY_USER;
+
+public class ScannerWsClientProvider {
+ static final int DEFAULT_CONNECT_TIMEOUT = 5;
+ static final int DEFAULT_RESPONSE_TIMEOUT = 0;
+ static final String READ_TIMEOUT_SEC_PROPERTY = "sonar.ws.timeout";
+ public static final String TOKEN_PROPERTY = "sonar.token";
+ private static final String TOKEN_ENV_VARIABLE = "SONAR_TOKEN";
+ static final int DEFAULT_READ_TIMEOUT_SEC = 60;
+ public static final String SONAR_SCANNER_PROXY_PORT = "sonar.scanner.proxyPort";
+ public static final String SONAR_SCANNER_CONNECT_TIMEOUT = "sonar.scanner.connectTimeout";
+ public static final String SONAR_SCANNER_SOCKET_TIMEOUT = "sonar.scanner.socketTimeout";
+ public static final String SONAR_SCANNER_RESPONSE_TIMEOUT = "sonar.scanner.responseTimeout";
+
+ @Bean("DefaultScannerWsClient")
+ public DefaultScannerWsClient provide(ScannerProperties scannerProps, EnvironmentInformation env, GlobalAnalysisMode globalMode,
+ System2 system, AnalysisWarnings analysisWarnings, SonarUserHome sonarUserHome) {
+ String url = defaultIfBlank(scannerProps.property("sonar.host.url"), "http://localhost:9000");
+ HttpConnector.Builder connectorBuilder = HttpConnector.newBuilder().acceptGzip(true);
+
+ String oldSocketTimeout = defaultIfBlank(scannerProps.property(READ_TIMEOUT_SEC_PROPERTY), valueOf(DEFAULT_READ_TIMEOUT_SEC));
+ String socketTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_SOCKET_TIMEOUT), oldSocketTimeout);
+ String connectTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_CONNECT_TIMEOUT), valueOf(DEFAULT_CONNECT_TIMEOUT));
+ String responseTimeout = defaultIfBlank(scannerProps.property(SONAR_SCANNER_RESPONSE_TIMEOUT), valueOf(DEFAULT_RESPONSE_TIMEOUT));
+ 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))
+ .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);
+ if (proxyHost != null) {
+ String proxyPortStr = defaultIfBlank(scannerProps.property(SONAR_SCANNER_PROXY_PORT), url.startsWith("https") ? "443" : "80");
+ var proxyPort = parseIntProperty(proxyPortStr, SONAR_SCANNER_PROXY_PORT);
+ connectorBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
+ }
+
+ var scannerProxyUser = scannerProps.property("sonar.scanner.proxyUser");
+ String proxyUser = scannerProxyUser != null ? scannerProxyUser : system.properties().getProperty(HTTP_PROXY_USER, "");
+ if (isNotBlank(proxyUser)) {
+ var scannerProxyPwd = scannerProps.property("sonar.scanner.proxyPassword");
+ String proxyPassword = scannerProxyPwd != null ? scannerProxyPwd : system.properties().getProperty(HTTP_PROXY_PASSWORD, "");
+ connectorBuilder.proxyCredentials(proxyUser, proxyPassword);
+ }
+
+ return new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connectorBuilder.build()), login != null, globalMode, analysisWarnings);
+ }
+
+ private static int parseIntProperty(String propValue, String propKey) {
+ try {
+ return parseInt(propValue);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(propKey + " is not a valid integer: " + propValue, e);
+ }
+ }
+
+ /**
+ * For testing, we can accept timeouts that are smaller than a second, expressed using ISO-8601 format for durations.
+ * If we can't parse as ISO-8601, then fallback to the official format that is simply the number of seconds
+ */
+ private static int parseDurationProperty(String propValue, String propKey) {
+ try {
+ return (int) Duration.parse(propValue).toMillis();
+ } catch (DateTimeParseException e) {
+ return parseIntProperty(propValue, propKey) * 1_000;
+ }
+ }
+
+ private static 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
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;
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;
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;
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;
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;
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;
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;
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 {
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;
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 {
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 {
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;
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);
<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
<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>
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;
+++ /dev/null
-/*
- * 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.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.List;
-import org.apache.commons.lang3.StringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.slf4j.event.Level;
-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.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.HttpException;
-import org.sonarqube.ws.client.MockWsResponse;
-import org.sonarqube.ws.client.WsClient;
-import org.sonarqube.ws.client.WsRequest;
-import org.sonarqube.ws.client.WsResponse;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
-
-public class DefaultScannerWsClientTest {
-
- @Rule
- public LogTester logTester = new LogTester();
-
- private final WsClient wsClient = mock(WsClient.class, Mockito.RETURNS_DEEP_STUBS);
-
- private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
-
- @Test
- public void call_whenDebugLevel_shouldLogAndProfileRequest() {
- WsRequest request = newRequest();
- WsResponse response = newResponse().setRequestUrl("https://local/api/issues/search");
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- logTester.setLevel(LoggerLevel.DEBUG);
- DefaultScannerWsClient underTest = new DefaultScannerWsClient(wsClient, false, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
-
- WsResponse result = underTest.call(request);
-
- // do not fail the execution -> interceptor returns the response
- assertThat(result).isSameAs(response);
-
- // check logs
- List<String> debugLogs = logTester.logs(Level.DEBUG);
- assertThat(debugLogs).hasSize(1);
- assertThat(debugLogs.get(0)).contains("GET 200 https://local/api/issues/search | time=");
- }
-
- @Test
- public void createErrorMessage_whenJsonError_shouldCreateErrorMsg() {
- String content = "{\"errors\":[{\"msg\":\"missing scan permission\"}, {\"msg\":\"missing another permission\"}]}";
- assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("missing scan permission, missing another permission");
- }
-
- @Test
- public void createErrorMessage_whenHtml_shouldCreateErrorMsg() {
- String content = "<!DOCTYPE html><html>something</html>";
- assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("HTTP code 400");
- }
-
- @Test
- public void createErrorMessage_whenLongContent_shouldCreateErrorMsg() {
- String content = StringUtils.repeat("mystring", 1000);
- assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).hasSize(15 + 128);
- }
-
- @Test
- public void call_whenUnauthorizedAndDebugEnabled_shouldLogResponseDetails() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setContent("Missing credentials")
- .setHeader("Authorization: ", "Bearer ImNotAValidToken")
- .setCode(403);
-
- logTester.setLevel(LoggerLevel.DEBUG);
-
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage(
- "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
-
- List<String> debugLogs = logTester.logs(Level.DEBUG);
- assertThat(debugLogs).hasSize(2);
- assertThat(debugLogs.get(1)).contains("Error response content: Missing credentials, headers: {Authorization: =[Bearer ImNotAValidToken]}");
- }
-
- @Test
- public void call_whenUnauthenticatedAndDebugEnabled_shouldLogResponseDetails() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setContent("Missing authentication")
- .setHeader("X-Test-Header: ", "ImATestHeader")
- .setCode(401);
-
- logTester.setLevel(LoggerLevel.DEBUG);
-
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
- "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
-
- List<String> debugLogs = logTester.logs(Level.DEBUG);
- assertThat(debugLogs).hasSize(2);
- assertThat(debugLogs.get(1)).contains("Error response content: Missing authentication, headers: {X-Test-Header: =[ImATestHeader]}");
- }
-
- @Test
- public void call_whenMissingCredentials_shouldFailWithMsg() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setContent("Missing authentication")
- .setCode(401);
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
- "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
- }
-
- @Test
- public void call_whenInvalidCredentials_shouldFailWithMsg() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setContent("Invalid credentials")
- .setCode(401);
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, /* credentials are configured */true,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage("Not authorized. Please check the user token in the property 'sonar.token' or the credentials in the properties 'sonar.login' and 'sonar.password'.");
- }
-
- @Test
- public void call_whenMissingPermissions_shouldFailWithMsg() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setContent("Unauthorized")
- .setCode(403);
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage(
- "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
- }
-
- @Test
- public void call_whenTokenExpirationApproaches_shouldLogWarnings() {
- WsRequest request = newRequest();
- var fiveDaysLatter = LocalDateTime.now().atZone(ZoneOffset.UTC).plusDays(5);
- String expirationDate = DateTimeFormatter
- .ofPattern(DATETIME_FORMAT)
- .format(fiveDaysLatter);
- WsResponse response = newResponse()
- .setCode(200)
- .setExpirationDate(expirationDate);
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- logTester.setLevel(LoggerLevel.DEBUG);
- DefaultScannerWsClient underTest = new DefaultScannerWsClient(wsClient, false, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- underTest.call(request);
- // the second call should not add the same warning twice
- underTest.call(request);
-
- // check logs
- List<String> warningLogs = logTester.logs(Level.WARN);
- assertThat(warningLogs).hasSize(2);
- assertThat(warningLogs.get(0)).contains("The token used for this analysis will expire on: " + fiveDaysLatter.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")));
- assertThat(warningLogs.get(1)).contains("Analysis executed with this token will fail after the expiration date.");
- }
-
- @Test
- public void call_whenBadRequest_shouldFailWithMessage() {
- WsRequest request = newRequest();
- WsResponse response = newResponse()
- .setCode(400)
- .setContent("{\"errors\":[{\"msg\":\"Boo! bad request! bad!\"}]}");
- when(wsClient.wsConnector().call(request)).thenReturn(response);
-
- DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
- new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
- assertThatThrownBy(() -> client.call(request))
- .isInstanceOf(MessageException.class)
- .hasMessage("Boo! bad request! bad!");
- }
-
- private MockWsResponse newResponse() {
- return new MockWsResponse().setRequestUrl("https://local/api/issues/search");
- }
-
- private WsRequest newRequest() {
- return new GetRequest("api/issues/search");
- }
-}
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);
}
*/
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);
}
@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,
}
@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");
}
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) {
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) {
}
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);
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;
+++ /dev/null
-/*
- * 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)))));
-
- }
-}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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;
--- /dev/null
+/*
+ * 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 java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.slf4j.event.Level;
+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;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsRequest;
+import org.sonarqube.ws.client.WsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
+
+public class DefaultScannerWsClientTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private final WsClient wsClient = mock(WsClient.class, Mockito.RETURNS_DEEP_STUBS);
+
+ private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
+
+ @Test
+ public void call_whenDebugLevel_shouldLogAndProfileRequest() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse().setRequestUrl("https://local/api/issues/search");
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ logTester.setLevel(LoggerLevel.DEBUG);
+ DefaultScannerWsClient underTest = new DefaultScannerWsClient(wsClient, false, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+
+ WsResponse result = underTest.call(request);
+
+ // do not fail the execution -> interceptor returns the response
+ assertThat(result).isSameAs(response);
+
+ // check logs
+ List<String> debugLogs = logTester.logs(Level.DEBUG);
+ assertThat(debugLogs).hasSize(1);
+ assertThat(debugLogs.get(0)).contains("GET 200 https://local/api/issues/search | time=");
+ }
+
+ @Test
+ public void createErrorMessage_whenJsonError_shouldCreateErrorMsg() {
+ String content = "{\"errors\":[{\"msg\":\"missing scan permission\"}, {\"msg\":\"missing another permission\"}]}";
+ assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("missing scan permission, missing another permission");
+ }
+
+ @Test
+ public void createErrorMessage_whenHtml_shouldCreateErrorMsg() {
+ String content = "<!DOCTYPE html><html>something</html>";
+ assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("HTTP code 400");
+ }
+
+ @Test
+ public void createErrorMessage_whenLongContent_shouldCreateErrorMsg() {
+ String content = StringUtils.repeat("mystring", 1000);
+ assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).hasSize(15 + 128);
+ }
+
+ @Test
+ public void call_whenUnauthorizedAndDebugEnabled_shouldLogResponseDetails() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setContent("Missing credentials")
+ .setHeader("Authorization: ", "Bearer ImNotAValidToken")
+ .setCode(403);
+
+ logTester.setLevel(LoggerLevel.DEBUG);
+
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage(
+ "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
+
+ List<String> debugLogs = logTester.logs(Level.DEBUG);
+ assertThat(debugLogs).hasSize(2);
+ assertThat(debugLogs.get(1)).contains("Error response content: Missing credentials, headers: {Authorization: =[Bearer ImNotAValidToken]}");
+ }
+
+ @Test
+ public void call_whenUnauthenticatedAndDebugEnabled_shouldLogResponseDetails() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setContent("Missing authentication")
+ .setHeader("X-Test-Header: ", "ImATestHeader")
+ .setCode(401);
+
+ logTester.setLevel(LoggerLevel.DEBUG);
+
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
+ "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+
+ List<String> debugLogs = logTester.logs(Level.DEBUG);
+ assertThat(debugLogs).hasSize(2);
+ assertThat(debugLogs.get(1)).contains("Error response content: Missing authentication, headers: {X-Test-Header: =[ImATestHeader]}");
+ }
+
+ @Test
+ public void call_whenMissingCredentials_shouldFailWithMsg() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setContent("Missing authentication")
+ .setCode(401);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
+ "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+ }
+
+ @Test
+ public void call_whenInvalidCredentials_shouldFailWithMsg() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setContent("Invalid credentials")
+ .setCode(401);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, /* credentials are configured */true,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage("Not authorized. Please check the user token in the property 'sonar.token' or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+ }
+
+ @Test
+ public void call_whenMissingPermissions_shouldFailWithMsg() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setContent("Unauthorized")
+ .setCode(403);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage(
+ "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
+ }
+
+ @Test
+ public void call_whenTokenExpirationApproaches_shouldLogWarnings() {
+ WsRequest request = newRequest();
+ var fiveDaysLatter = LocalDateTime.now().atZone(ZoneOffset.UTC).plusDays(5);
+ String expirationDate = DateTimeFormatter
+ .ofPattern(DATETIME_FORMAT)
+ .format(fiveDaysLatter);
+ WsResponse response = newResponse()
+ .setCode(200)
+ .setExpirationDate(expirationDate);
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ logTester.setLevel(LoggerLevel.DEBUG);
+ DefaultScannerWsClient underTest = new DefaultScannerWsClient(wsClient, false, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ underTest.call(request);
+ // the second call should not add the same warning twice
+ underTest.call(request);
+
+ // check logs
+ List<String> warningLogs = logTester.logs(Level.WARN);
+ assertThat(warningLogs).hasSize(2);
+ assertThat(warningLogs.get(0)).contains("The token used for this analysis will expire on: " + fiveDaysLatter.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")));
+ assertThat(warningLogs.get(1)).contains("Analysis executed with this token will fail after the expiration date.");
+ }
+
+ @Test
+ public void call_whenBadRequest_shouldFailWithMessage() {
+ WsRequest request = newRequest();
+ WsResponse response = newResponse()
+ .setCode(400)
+ .setContent("{\"errors\":[{\"msg\":\"Boo! bad request! bad!\"}]}");
+ when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+ DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
+ new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+ assertThatThrownBy(() -> client.call(request))
+ .isInstanceOf(MessageException.class)
+ .hasMessage("Boo! bad request! bad!");
+ }
+
+ private MockWsResponse newResponse() {
+ return new MockWsResponse().setRequestUrl("https://local/api/issues/search");
+ }
+
+ private WsRequest newRequest() {
+ return new GetRequest("api/issues/search");
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
--- /dev/null
+## 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
--- /dev/null
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
+53j8dPRgcqZGjgO/EG0uAj3DINgNOg==
+-----END PRIVATE KEY-----
--- /dev/null
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
++iNXE0r4lx4TmqctoBApwtiahBnPfg==
+-----END PRIVATE KEY-----
--- /dev/null
+-----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-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
+vsseryZb70+/NjZqWcSTcuFhooe9+Q==
+-----END PRIVATE KEY-----
--- /dev/null
+-----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-----
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+-----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-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
+9uPzaGE09HfpBsTJtUTjU0mClVlOCw==
+-----END PRIVATE KEY-----
--- /dev/null
+-----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-----
--- /dev/null
+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