]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22038 Support new SSL properties (#10987)
authorJulien HENRY <julien.henry@sonarsource.com>
Tue, 23 Apr 2024 07:22:26 +0000 (09:22 +0200)
committerMatteo Mara <matteo.mara@sonarsource.com>
Tue, 30 Apr 2024 08:58:03 +0000 (10:58 +0200)
* Move scanner HttpClient code in its own package
* Factorize the computation of the Sonar User Home

73 files changed:
build.gradle
sonar-scanner-engine/build.gradle
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/GlobalTempFolderProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/PluginFiles.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerPluginInstaller.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringGlobalContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/platform/DefaultServer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/qualitygate/QualityGateCheck.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/DefaultQualityProfileLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/language/DefaultLanguagesLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/AbstractSettingsLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/rule/DefaultActiveRulesLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/DeprecatedPropertiesWarningGenerator.java
sonar-scanner-engine/src/main/resources/logback.xml
sonar-scanner-engine/src/main/resources/org/sonar/batch/bootstrapper/logback.xml
sonar-scanner-engine/src/test/java/org/sonar/scanner/WsTestUtil.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java [deleted file]
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/GlobalTempFolderProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/PluginFilesTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerPluginInstallerTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java [deleted file]
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/cache/DefaultAnalysisCacheLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/platform/DefaultServerTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/qualitygate/QualityGateCheckTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultMetricsRepositoryLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultNewCodePeriodLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultProjectRepositoriesLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/DefaultQualityProfileLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/language/DefaultLanguagesRepositoryTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultGlobalSettingsLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultProjectSettingsLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/rule/DefaultActiveRulesLoaderTest.java
sonar-scanner-engine/src/test/resources/ssl/README.md [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/ca.crt [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/ca.key [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12 [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/client.csr [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/client.key [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/client.p12 [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/client.pem [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/openssl.conf [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12 [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/server.csr [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/server.key [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/server.p12 [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/server.pem [new file with mode: 0644]
sonar-scanner-engine/src/test/resources/ssl/v3.ext [new file with mode: 0644]

index a4a32bbb0ea78a968e06a6332ea0188e01df5939..408182a06e08cb35a53585f7bb616b8fbbb2e7ac 100644 (file)
@@ -315,6 +315,7 @@ subprojects {
         exclude 'junit:junit'
       }
       dependency 'com.squareup.okio:okio:3.7.0'
+      dependency 'io.github.hakky54:sslcontext-kickstart:8.3.4'
       dependency 'io.prometheus:simpleclient:0.16.0'
       dependency 'io.prometheus:simpleclient_common:0.16.0'
       dependency 'io.prometheus:simpleclient_servlet:0.16.0'
@@ -370,6 +371,7 @@ subprojects {
         entry 'junit-jupiter-params'
         entry 'junit-vintage-engine'
       }
+      dependency 'org.junit-pioneer:junit-pioneer:2.2.0'
       dependency 'org.xmlunit:xmlunit-core:2.9.1'
       dependency 'org.xmlunit:xmlunit-matchers:2.9.1'
       dependency 'org.lz4:lz4-java:1.8.0'
index af2349194a54113567bc614b53e428ebac7d2c5e..e6a289228b8ae8a164b5aec27682e67341900880 100644 (file)
@@ -27,6 +27,7 @@ dependencies {
   api 'com.google.protobuf:protobuf-java'
   api 'com.squareup.okhttp3:okhttp'
   api 'com.fasterxml.staxmate:staxmate'
+  implementation 'io.github.hakky54:sslcontext-kickstart'
   api 'javax.annotation:javax.annotation-api'
   api 'org.eclipse.jgit:org.eclipse.jgit'
   api 'org.tmatesoft.svnkit:svnkit'
@@ -64,6 +65,7 @@ dependencies {
   testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
   testImplementation project(':plugins:sonar-xoo-plugin')
   testImplementation 'org.wiremock:wiremock-standalone'
+  testImplementation 'org.junit-pioneer:junit-pioneer'
 
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
   testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java
deleted file mode 100644 (file)
index 1d192ad..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.bootstrap;
-
-import com.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>");
-  }
-}
index 821529b3d6064490901037a3bfbce58dfb282128..07711d1b782e9182ddf941263869aede38739504 100644 (file)
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.impl.utils.DefaultTempFolder;
-import org.sonar.api.utils.System2;
 import org.sonar.api.utils.TempFolder;
 import org.springframework.context.annotation.Bean;
 
@@ -42,24 +41,13 @@ public class GlobalTempFolderProvider {
   private static final long CLEAN_MAX_AGE = TimeUnit.DAYS.toMillis(21);
   static final String TMP_NAME_PREFIX = ".sonartmp_";
 
-  private System2 system;
-
-  public GlobalTempFolderProvider() {
-    this(new System2());
-  }
-
-  GlobalTempFolderProvider(System2 system) {
-    this.system = system;
-  }
-
   @Bean("GlobalTempFolder")
-  public TempFolder provide(ScannerProperties scannerProps) {
-
-    String workingPathName = StringUtils.defaultIfBlank(scannerProps.property(CoreProperties.GLOBAL_WORKING_DIRECTORY), CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
-    Path workingPath = Paths.get(workingPathName);
+  public TempFolder provide(ScannerProperties scannerProps, SonarUserHome userHome) {
+    var workingPathName = StringUtils.defaultIfBlank(scannerProps.property(CoreProperties.GLOBAL_WORKING_DIRECTORY), CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
+    var workingPath = Paths.get(workingPathName);
 
     if (!workingPath.isAbsolute()) {
-      Path home = findSonarHome(scannerProps);
+      var home = userHome.getPath();
       workingPath = home.resolve(workingPath).normalize();
     }
     try {
@@ -67,7 +55,7 @@ public class GlobalTempFolderProvider {
     } catch (IOException e) {
       LOG.error(String.format("failed to clean global working directory: %s", workingPath), e);
     }
-    Path tempDir = createTempFolder(workingPath);
+    var tempDir = createTempFolder(workingPath);
     return new DefaultTempFolder(tempDir.toFile(), true);
 
   }
@@ -90,24 +78,8 @@ public class GlobalTempFolderProvider {
     }
   }
 
-  private Path findSonarHome(ScannerProperties props) {
-    String home = props.property("sonar.userHome");
-    if (home != null) {
-      return Paths.get(home).toAbsolutePath();
-    }
-
-    home = system.envVariable("SONAR_USER_HOME");
-
-    if (home != null) {
-      return Paths.get(home).toAbsolutePath();
-    }
-
-    home = system.property("user.home");
-    return Paths.get(home, ".sonar").toAbsolutePath();
-  }
-
   private static void cleanTempFolders(Path path) throws IOException {
-    if (path.toFile().exists()) {
+    if (Files.exists(path)) {
       try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, new CleanFilter())) {
         for (Path p : stream) {
           deleteQuietly(p.toFile());
@@ -118,8 +90,8 @@ public class GlobalTempFolderProvider {
 
   private static class CleanFilter implements DirectoryStream.Filter<Path> {
     @Override
-    public boolean accept(Path path) throws IOException {
-      if (!path.toFile().exists()) {
+    public boolean accept(Path path) {
+      if (!Files.exists(path)) {
         return false;
       }
 
index 727ada7fbb29c4fbe898a13a1aecf62eb013f088..ba53811a9a8252eb59de0f0ff7df751aedf3aabc 100644 (file)
@@ -26,15 +26,16 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.nio.file.Files;
-import java.util.Objects;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.util.Optional;
-import java.util.stream.Stream;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.io.FileUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.config.Configuration;
 import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpException;
 import org.sonarqube.ws.client.WsResponse;
@@ -51,21 +52,21 @@ public class PluginFiles {
 
   private final DefaultScannerWsClient wsClient;
   private final Configuration configuration;
-  private final File cacheDir;
-  private final File tempDir;
+  private final Path cacheDir;
+  private final Path tempDir;
 
-  public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration) {
+  public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration, SonarUserHome sonarUserHome) {
     this.wsClient = wsClient;
     this.configuration = configuration;
-    File home = locateHomeDir(configuration);
-    this.cacheDir = mkdir(new File(home, "cache"), "user cache");
-    this.tempDir = mkdir(new File(home, "_tmp"), "temp dir");
-    LOGGER.info("User cache: {}", cacheDir.getAbsolutePath());
+    var home = sonarUserHome.getPath();
+    this.cacheDir = mkdir(home.resolve("cache"), "user cache");
+    this.tempDir = mkdir(home.resolve("_tmp"), "temp dir");
+    LOGGER.info("User cache: {}", cacheDir);
   }
 
   public File createTempDir() {
     try {
-      return Files.createTempDirectory(tempDir.toPath(), "plugins").toFile();
+      return Files.createTempDirectory(tempDir, "plugins").toFile();
     } catch (IOException e) {
       throw new IllegalStateException("Fail to create temp directory in " + tempDir, e);
     }
@@ -81,24 +82,24 @@ public class PluginFiles {
    */
   public Optional<File> get(InstalledPlugin plugin) {
     // Does not fail if another process tries to create the directory at the same time.
-    File jarInCache = jarInCache(plugin.key, plugin.hash);
-    if (jarInCache.exists() && jarInCache.isFile()) {
-      return Optional.of(jarInCache);
+    Path jarInCache = jarInCache(plugin.key, plugin.hash);
+    if (Files.isRegularFile(jarInCache)) {
+      return Optional.of(jarInCache.toFile());
     }
-    return download(plugin);
+    return download(plugin).map(Path::toFile);
   }
 
-  private Optional<File> download(InstalledPlugin plugin) {
+  private Optional<Path> download(InstalledPlugin plugin) {
     GetRequest request = new GetRequest("api/plugins/download")
       .setParam("plugin", plugin.key)
       .setTimeOutInMs(configuration.getInt(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY).orElse(PLUGINS_DOWNLOAD_TIMEOUT_DEFAULT) * 1000);
 
-    File downloadedFile = newTempFile();
+    Path downloadedFile = newTempFile();
     LOGGER.debug("Download plugin '{}' to '{}'", plugin.key, downloadedFile);
 
     try (WsResponse response = wsClient.call(request)) {
       Optional<String> expectedMd5 = response.header(MD5_HEADER);
-      if (!expectedMd5.isPresent()) {
+      if (expectedMd5.isEmpty()) {
         throw new IllegalStateException(format(
           "Fail to download plugin [%s]. Request to %s did not return header %s", plugin.key, response.requestUrl(), MD5_HEADER));
       }
@@ -114,14 +115,14 @@ public class PluginFiles {
 
       // un-compress if needed
       String cacheMd5;
-      File tempJar;
+      Path tempJar;
 
       tempJar = downloadedFile;
       cacheMd5 = expectedMd5.get();
 
       // put in cache
-      File jarInCache = jarInCache(plugin.key, cacheMd5);
-      mkdir(jarInCache.getParentFile());
+      Path jarInCache = jarInCache(plugin.key, cacheMd5);
+      mkdir(jarInCache.getParent());
       moveFile(tempJar, jarInCache);
       return Optional.of(jarInCache);
 
@@ -137,82 +138,75 @@ public class PluginFiles {
     }
   }
 
-  private static void downloadBinaryTo(InstalledPlugin plugin, File downloadedFile, WsResponse response) {
+  private static void downloadBinaryTo(InstalledPlugin plugin, Path downloadedFile, WsResponse response) {
     try (InputStream stream = response.contentStream()) {
-      FileUtils.copyInputStreamToFile(stream, downloadedFile);
+      FileUtils.copyInputStreamToFile(stream, downloadedFile.toFile());
     } catch (IOException e) {
       throw new IllegalStateException(format("Fail to download plugin [%s] into %s", plugin.key, downloadedFile), e);
     }
   }
 
-  private File jarInCache(String pluginKey, String hash) {
-    File hashDir = new File(cacheDir, hash);
-    File file = new File(hashDir, format("sonar-%s-plugin.jar", pluginKey));
-    if (!file.getParentFile().toPath().equals(hashDir.toPath())) {
+  private Path jarInCache(String pluginKey, String hash) {
+    Path hashDir = cacheDir.resolve(hash);
+    Path file = hashDir.resolve(format("sonar-%s-plugin.jar", pluginKey));
+    if (!file.getParent().equals(hashDir)) {
       // vulnerability - attempt to create a file outside the cache directory
       throw new IllegalStateException(format("Fail to download plugin [%s]. Key is not valid.", pluginKey));
     }
     return file;
   }
 
-  private File newTempFile() {
+  private Path newTempFile() {
     try {
-      return File.createTempFile("fileCache", null, tempDir);
+      return Files.createTempFile(tempDir, "fileCache", null);
     } catch (IOException e) {
       throw new IllegalStateException("Fail to create temp file in " + tempDir, e);
     }
   }
 
-  private static String computeMd5(File file) {
-    try (InputStream fis = new BufferedInputStream(FileUtils.openInputStream(file))) {
+  private static String computeMd5(Path file) {
+    try (InputStream fis = new BufferedInputStream(Files.newInputStream(file))) {
       return DigestUtils.md5Hex(fis);
     } catch (IOException e) {
       throw new IllegalStateException("Fail to compute md5 of " + file, e);
     }
   }
 
-  private static void moveFile(File sourceFile, File targetFile) {
-    boolean rename = sourceFile.renameTo(targetFile);
-    // Check if the file was cached by another process during download
-    if (!rename && !targetFile.exists()) {
-      LOGGER.warn("Unable to rename {} to {}", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
-      LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
-      try {
-        Files.move(sourceFile.toPath(), targetFile.toPath());
-      } catch (IOException e) {
-        throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
+  private static void moveFile(Path sourceFile, Path targetFile) {
+    try {
+      Files.move(sourceFile, targetFile, StandardCopyOption.ATOMIC_MOVE);
+    } catch (IOException e1) {
+      // Check if the file was cached by another process during download
+      if (!Files.exists(targetFile)) {
+        LOGGER.warn("Unable to rename {} to {}", sourceFile, targetFile);
+        LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
+        try {
+          Files.move(sourceFile, targetFile);
+        } catch (IOException e2) {
+          throw new IllegalStateException("Fail to move " + sourceFile + " to " + targetFile, e2);
+        }
       }
     }
   }
 
-  private static void mkdir(File dir) {
+  private static void mkdir(Path dir) {
     try {
-      Files.createDirectories(dir.toPath());
+      Files.createDirectories(dir);
     } catch (IOException e) {
       throw new IllegalStateException("Fail to create cache directory: " + dir, e);
     }
   }
 
-  private static File mkdir(File dir, String debugTitle) {
-    if (!dir.isDirectory() || !dir.exists()) {
-      LOGGER.debug("Create : {}", dir.getAbsolutePath());
+  private static Path mkdir(Path dir, String debugTitle) {
+    if (!Files.isDirectory(dir)) {
+      LOGGER.debug("Create : {}", dir);
       try {
-        Files.createDirectories(dir.toPath());
+        Files.createDirectories(dir);
       } catch (IOException e) {
-        throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
+        throw new IllegalStateException("Unable to create folder " + debugTitle + " at " + dir, e);
       }
     }
     return dir;
   }
 
-  private static File locateHomeDir(Configuration configuration) {
-    return Stream.of(
-      configuration.get("sonar.userHome").orElse(null),
-      System.getenv("SONAR_USER_HOME"),
-      System.getProperty("user.home") + File.separator + ".sonar")
-      .filter(Objects::nonNull)
-      .findFirst()
-      .map(File::new)
-      .get();
-  }
 }
index 9bf367f630d07aad5eb8cde8c2bcd803a3a4b6d2..081410361d6c02a539143885f00358022315e0ff 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
 import org.sonar.core.platform.PluginInfo;
 import org.sonar.core.plugin.PluginType;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.mediumtest.LocalPlugin;
 import org.sonarqube.ws.client.GetRequest;
 
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClient.java
deleted file mode 100644 (file)
index 280fb12..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.bootstrap;
-
-import org.sonarqube.ws.client.WsRequest;
-import org.sonarqube.ws.client.WsResponse;
-
-public interface ScannerWsClient {
-  WsResponse call(WsRequest request);
-
-  String baseUrl();
-
-}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerWsClientProvider.java
deleted file mode 100644 (file)
index 2d8f732..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.bootstrap;
-
-import 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;
-    }
-  }
-}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHome.java
new file mode 100644 (file)
index 0000000..afbc0bb
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.bootstrap;
+
+import java.nio.file.Path;
+
+public class SonarUserHome {
+
+  private final Path userHome;
+
+  public SonarUserHome(Path userHome) {
+    this.userHome = userHome;
+  }
+
+  public Path getPath() {
+    return userHome;
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SonarUserHomeProvider.java
new file mode 100644 (file)
index 0000000..da97e01
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.bootstrap;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
+import org.springframework.context.annotation.Bean;
+
+public class SonarUserHomeProvider {
+  private static final Logger LOG = LoggerFactory.getLogger(SonarUserHomeProvider.class);
+
+  private System2 system;
+
+  public SonarUserHomeProvider() {
+    this(new System2());
+  }
+
+  SonarUserHomeProvider(System2 system) {
+    this.system = system;
+  }
+
+  @Bean
+  public SonarUserHome provide(ScannerProperties scannerProps) {
+    Path home = findSonarHome(scannerProps);
+    LOG.debug("Sonar User Home: {}", home);
+    return new SonarUserHome(home);
+  }
+
+  private Path findSonarHome(ScannerProperties props) {
+    var home = props.property("sonar.userHome");
+    if (home != null) {
+      return Paths.get(home).toAbsolutePath();
+    }
+
+    home = system.envVariable("SONAR_USER_HOME");
+
+    if (home != null) {
+      return Paths.get(home).toAbsolutePath();
+    }
+
+    var userHome = Objects.requireNonNull(system.property("user.home"), "The system property 'user.home' is expected to be non null");
+    return Paths.get(userHome, ".sonar").toAbsolutePath();
+  }
+
+}
index 69a430dd8699de33165cd8bad86f477882f387c2..b9de4071df23b8dd0a6bee2157b5b9bb52309646 100644 (file)
@@ -48,6 +48,7 @@ import org.sonar.core.platform.SpringComponentContainer;
 import org.sonar.core.util.DefaultHttpDownloader;
 import org.sonar.core.util.UuidFactoryImpl;
 import org.sonar.scanner.extension.ScannerCoreExtensionsInstaller;
+import org.sonar.scanner.http.ScannerWsClientProvider;
 import org.sonar.scanner.notifications.DefaultAnalysisWarnings;
 import org.sonar.scanner.platform.DefaultServer;
 import org.sonar.scanner.repository.DefaultMetricsRepositoryLoader;
@@ -98,6 +99,7 @@ public class SpringGlobalContainer extends SpringComponentContainer {
       DefaultServer.class,
       DefaultDocumentationLinkGenerator.class,
       new GlobalTempFolderProvider(),
+      new SonarUserHomeProvider(),
       analysisWarnings,
       UriReader.class,
       PluginFiles.class,
index f797a2cfdeca2f5969595124c90dc19b8de07953..33037ede59619b55377b6f03ccb7b8a88caeb37d 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
 import org.sonar.core.util.Protobuf;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.protocol.internal.ScannerInternal.SensorCacheEntry;
 import org.sonar.scanner.protocol.internal.SensorCacheData;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/DefaultScannerWsClient.java
new file mode 100644 (file)
index 0000000..a8093c8
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * 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>");
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClient.java
new file mode 100644 (file)
index 0000000..9b49886
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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();
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ScannerWsClientProvider.java
new file mode 100644 (file)
index 0000000..2511daf
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * 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();
+  }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/package-info.java
new file mode 100644 (file)
index 0000000..349a606
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.scanner.http;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/CertificateStore.java
new file mode 100644 (file)
index 0000000..b285d3b
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.http.ssl;
+
+import java.nio.file.Path;
+
+public class CertificateStore {
+  public static final String DEFAULT_PASSWORD = "sonar";
+  public static final String DEFAULT_STORE_TYPE = "PKCS12";
+  private final Path path;
+  private final String keyStorePassword;
+  private final String keyStoreType;
+
+  public CertificateStore(Path path, String keyStorePassword) {
+    this.path = path;
+    this.keyStorePassword = keyStorePassword;
+    this.keyStoreType = DEFAULT_STORE_TYPE;
+  }
+
+  public Path getPath() {
+    return path;
+  }
+
+  public String getKeyStorePassword() {
+    return keyStorePassword;
+  }
+
+  public String getKeyStoreType() {
+    return keyStoreType;
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/SslConfig.java
new file mode 100644 (file)
index 0000000..8f172ca
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.http.ssl;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class SslConfig {
+  private final CertificateStore keyStore;
+  private final CertificateStore trustStore;
+
+  public SslConfig(@Nullable CertificateStore keyStore, @Nullable CertificateStore trustStore) {
+    this.keyStore = keyStore;
+    this.trustStore = trustStore;
+  }
+
+  @CheckForNull
+  public CertificateStore getKeyStore() {
+    return keyStore;
+  }
+
+  @CheckForNull
+  public CertificateStore getTrustStore() {
+    return trustStore;
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/http/ssl/package-info.java
new file mode 100644 (file)
index 0000000..e1e4c18
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.scanner.http.ssl;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 9ec8ac9ae9c33ca9cc1bbc1e0b1b581d990842ba..a53f3de7f85535f7008f381c89cf192249f6f254 100644 (file)
@@ -26,7 +26,7 @@ import org.sonar.api.config.Configuration;
 import org.sonar.api.platform.Server;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.core.platform.SonarQubeVersion;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 import static org.apache.commons.lang3.StringUtils.trimToEmpty;
 
index 7dce5a4bee50054a641c4a5b4f5da7ecf5ea0e4f..aa4c30d352b884fe4a29a0e44fae08af07f49290 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.api.Startable;
 import org.sonar.api.utils.MessageException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
 import org.sonar.scanner.report.CeTaskReportDataHolder;
 import org.sonar.scanner.scan.ScanProperties;
index 2f1787885ad2b1767fe9f2501875788bc366c6b3..15f12eef49d7800fa60491c96cff6c6d0baec3da 100644 (file)
@@ -43,7 +43,7 @@ import org.sonar.api.utils.MessageException;
 import org.sonar.api.utils.TempFolder;
 import org.sonar.api.utils.ZipUtils;
 import org.sonar.core.ce.CeTaskCharacteristics;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
 import org.sonar.scanner.ci.CiConfiguration;
 import org.sonar.scanner.fs.InputModuleHierarchy;
index a69c13e2b0c86fef075a4db263e451729019e5e3..df65d94fa9816e4ebc1b9368cafe54be20a8db5f 100644 (file)
@@ -25,7 +25,7 @@ import java.util.ArrayList;
 import java.util.List;
 import org.sonar.api.measures.Metric;
 import org.sonar.api.measures.Metric.ValueType;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.protocol.GsonHelper;
 import org.sonarqube.ws.client.GetRequest;
 
index 2a6b78be421f409e1d4a68df52c02c0a9d5615d1..6403b5fcdaa36ff5d2b5b740c10c6cc18dbfe768 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.scanner.repository;
 
 import java.io.IOException;
 import java.io.InputStream;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.NewCodePeriods;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpException;
index a35b66568e1bd7dfbf4e18d3e7818a6d003e23e8..b6c681cdb120644eeebc3c2b7da1cd91e3b0f6b8 100644 (file)
@@ -28,7 +28,7 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.impl.utils.ScannerUtils;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Batch.WsProjectResponse;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpException;
index 30c716fc05a9564d083cfb20e8445af10f10e640..cc5c2caa26489a96b694be359b19ff23042cdbf3 100644 (file)
@@ -28,7 +28,7 @@ import java.util.Map;
 import java.util.function.BinaryOperator;
 import java.util.function.Supplier;
 import org.sonar.api.utils.MessageException;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Qualityprofiles.SearchWsResponse;
 import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
 import org.sonarqube.ws.client.GetRequest;
index 9b004f39e8be4592fa0e46b4ddc18aa6fbf4172b..58aeb9695f8815c7d5233fb514b52856c777b56e 100644 (file)
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.config.Configuration;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.client.GetRequest;
 
 public class DefaultLanguagesLoader implements LanguagesLoader {
index 6df3e293910e30b3806d9c7ec526a86678169482..61b68ce9155eb9ca1b6c4ec9118a0d2b5dc1d805 100644 (file)
@@ -34,7 +34,7 @@ import org.sonar.api.impl.utils.ScannerUtils;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Settings;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpException;
index e5fabcfe48e3fbe015835979c528092b1db8b0ea..04d1feb1e9521dacb09046636b789b177e935830 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.scanner.repository.settings;
 
 import java.util.Map;
 import javax.inject.Inject;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 public class DefaultGlobalSettingsLoader extends AbstractSettingsLoader implements GlobalSettingsLoader {
 
index e03e92d82e04c55d1eeec97943d3dad044930812..61b0d6692484b49ac297475ad571ea24b0bbca34 100644 (file)
@@ -20,7 +20,7 @@
 package org.sonar.scanner.repository.settings;
 
 import java.util.Map;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.ScannerProperties;
 
 public class DefaultProjectSettingsLoader extends AbstractSettingsLoader implements ProjectSettingsLoader {
index 242ace13d31af89ed626ffa2b720313ec46e3066..1bc34c0198c34ac57477fd8a74710dc86c115cf0 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.api.batch.rule.LoadedActiveRule;
 import org.sonar.api.impl.utils.ScannerUtils;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.scanner.bootstrap.ScannerWsClient;
+import org.sonar.scanner.http.ScannerWsClient;
 import org.sonarqube.ws.Common.Paging;
 import org.sonarqube.ws.Rules;
 import org.sonarqube.ws.Rules.Active;
index 244fbe4aea94ff9e1caa1d166c12650d33a51804..2022782148ceb4761f40445e6937baf39d04d999 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.api.CoreProperties;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.notifications.AnalysisWarnings;
 import org.sonar.batch.bootstrapper.EnvironmentInformation;
-import org.sonar.scanner.bootstrap.ScannerWsClientProvider;
+import org.sonar.scanner.http.ScannerWsClientProvider;
 
 public class DeprecatedPropertiesWarningGenerator {
   private static final Logger LOG = LoggerFactory.getLogger(DeprecatedPropertiesWarningGenerator.class);
index c64f28abec76c4025db2e91f7354cfab27b939b7..ccd0dfe09b9f6dde35d05c6177ca92550308610a 100644 (file)
@@ -26,4 +26,9 @@
   <logger name="org.springframework.context.annotation.AnnotationConfigApplicationContext" level="ERROR"/>
 
   <logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
+
+  <!-- CertificateUtils is too verbose when loading system certificates -->
+  <logger name="nl.altindag.ssl.util.CertificateUtils" level="INFO"/>
+
+
 </configuration>
\ No newline at end of file
index ccf804462b6d8b702ed65d3a7a459ae4365da4b8..37d6c262d1d4611a46fd640ad80e05a06290add1 100644 (file)
@@ -36,4 +36,7 @@
 
   <logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
 
+  <!-- CertificateUtils is too verbose when loading system certificates -->
+  <logger name="nl.altindag.ssl.util.CertificateUtils" level="INFO"/>
+
 </configuration>
index cac8b43768984b463c3d52ad60be944412d7d175..6fb5829eb801491f40fa0a8f6e3eeecf4adf6f9c 100644 (file)
@@ -24,7 +24,7 @@ import java.io.Reader;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.mockito.ArgumentMatcher;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.client.WsRequest;
 import org.sonarqube.ws.client.WsResponse;
 
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java
deleted file mode 100644 (file)
index 07bcae1..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.bootstrap;
-
-import 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");
-  }
-}
index 19a251859ae035fa73e02d6ece0fd20f1c8d7693..15ee7b69856b118792595f5d8c00ec2fc071826e 100644 (file)
@@ -23,139 +23,107 @@ import com.google.common.collect.ImmutableMap;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributeView;
 import java.nio.file.attribute.FileTime;
-import java.util.Collections;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
-import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.utils.System2;
 import org.sonar.api.utils.TempFolder;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-public class GlobalTempFolderProviderTest {
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
+class GlobalTempFolderProviderTest {
 
-  private GlobalTempFolderProvider tempFolderProvider = new GlobalTempFolderProvider();
+  private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
+  private final GlobalTempFolderProvider underTest = new GlobalTempFolderProvider();
 
   @Test
-  public void createTempFolderProps() throws Exception {
-    File workingDir = temp.newFolder();
-    workingDir.delete();
+  void createTempFolderProps(@TempDir Path workingDir) throws Exception {
+    Files.delete(workingDir);
 
-    TempFolder tempFolder = tempFolderProvider.provide(
-      new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
+    var tempFolder = underTest.provide(
+      new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.toAbsolutePath().toString())), sonarUserHome);
     tempFolder.newDir();
     tempFolder.newFile();
-    assertThat(getCreatedTempDir(workingDir)).exists();
-    assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
 
-    FileUtils.deleteQuietly(workingDir);
+    assertThat(workingDir).isDirectory();
+    assertThat(workingDir.toFile().list()).hasSize(1);
+    var rootTmpDir = workingDir.toFile().listFiles()[0];
+    assertThat(rootTmpDir.list()).hasSize(2);
   }
 
   @Test
-  public void cleanUpOld() throws IOException {
+  void cleanUpOld(@TempDir Path workingDir) throws IOException {
     long creationTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(100);
-    File workingDir = temp.newFolder();
 
     for (int i = 0; i < 3; i++) {
-      File tmp = new File(workingDir, ".sonartmp_" + i);
-      tmp.mkdirs();
+      Path tmp = workingDir.resolve(".sonartmp_" + i);
+      Files.createDirectories(tmp);
       setFileCreationDate(tmp, creationTime);
     }
 
-    tempFolderProvider.provide(
-      new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.getAbsolutePath())));
-    // this also checks that all other temps were deleted
-    assertThat(getCreatedTempDir(workingDir)).exists();
+    underTest.provide(
+      new ScannerProperties(ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, workingDir.toAbsolutePath().toString())), sonarUserHome);
 
-    FileUtils.deleteQuietly(workingDir);
+    // this also checks that all other temps were deleted
+    assertThat(workingDir.toFile().list()).hasSize(1);
   }
 
   @Test
-  public void createTempFolderSonarHome() throws Exception {
+  void createTempFolderFromSonarHome(@TempDir Path sonarUserHomePath) throws Exception {
     // with sonar home, it will be in {sonar.home}/.sonartmp
-    File sonarHome = temp.newFolder();
-    File workingDir = new File(sonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
+    when(sonarUserHome.getPath()).thenReturn(sonarUserHomePath);
+    
+    var expectedWorkingDir = sonarUserHomePath.resolve(CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE);
 
-    TempFolder tempFolder = tempFolderProvider.provide(
-      new ScannerProperties(ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath())));
+    TempFolder tempFolder = underTest.provide(new ScannerProperties(Map.of()), sonarUserHome);
     tempFolder.newDir();
     tempFolder.newFile();
-    assertThat(getCreatedTempDir(workingDir)).exists();
-    assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
 
-    FileUtils.deleteQuietly(sonarHome);
+    assertThat(expectedWorkingDir).isDirectory();
+    assertThat(expectedWorkingDir.toFile().list()).hasSize(1);
+    var rootTmpDir = expectedWorkingDir.toFile().listFiles()[0];
+    assertThat(rootTmpDir.list()).hasSize(2);
   }
 
   @Test
-  public void createTempFolderDefault() throws Exception {
-    System2 system = mock(System2.class);
-    tempFolderProvider = new GlobalTempFolderProvider(system);
-    File userHome = temp.newFolder();
-
-    when(system.envVariable("SONAR_USER_HOME")).thenReturn(null);
-    when(system.property("user.home")).thenReturn(userHome.getAbsolutePath());
-
-    // if nothing is defined, it will be in {user.home}/.sonar/.sonartmp
-    File defaultSonarHome = new File(userHome.getAbsolutePath(), ".sonar");
-    File workingDir = new File(defaultSonarHome, CoreProperties.GLOBAL_WORKING_DIRECTORY_DEFAULT_VALUE).getAbsoluteFile();
-    try {
-      TempFolder tempFolder = tempFolderProvider.provide(
-        new ScannerProperties(Collections.emptyMap()));
-      tempFolder.newDir();
-      tempFolder.newFile();
-      assertThat(getCreatedTempDir(workingDir)).exists();
-      assertThat(getCreatedTempDir(workingDir).list()).hasSize(2);
-    } finally {
-      FileUtils.deleteQuietly(workingDir);
-    }
-  }
-
-  @Test
-  public void dotWorkingDir() {
-    File sonarHome = temp.getRoot();
+  void dotWorkingDir(@TempDir Path sonarUserHomePath) {
+    when(sonarUserHome.getPath()).thenReturn(sonarUserHomePath);
     String globalWorkDir = ".";
     ScannerProperties globalProperties = new ScannerProperties(
-      ImmutableMap.of("sonar.userHome", sonarHome.getAbsolutePath(), CoreProperties.GLOBAL_WORKING_DIRECTORY, globalWorkDir));
+      ImmutableMap.of(CoreProperties.GLOBAL_WORKING_DIRECTORY, globalWorkDir));
 
-    TempFolder tempFolder = tempFolderProvider.provide(globalProperties);
+    var tempFolder = underTest.provide(globalProperties, sonarUserHome);
     File newFile = tempFolder.newFile();
-    assertThat(newFile.getParentFile().getParentFile().getAbsolutePath()).isEqualTo(sonarHome.getAbsolutePath());
+    
+    assertThat(newFile.getParentFile().getParentFile().toPath()).isEqualTo(sonarUserHomePath);
     assertThat(newFile.getParentFile().getName()).startsWith(".sonartmp_");
   }
 
   @Test
-  public void homeIsSymbolicLink() throws IOException {
+  void homeIsSymbolicLink(@TempDir Path realSonarHome, @TempDir Path symlink) throws IOException {
     assumeTrue(!System2.INSTANCE.isOsWindows());
-    File realSonarHome = temp.newFolder();
-    File symlink = temp.newFolder();
-    symlink.delete();
-    Files.createSymbolicLink(symlink.toPath(), realSonarHome.toPath());
-    ScannerProperties globalProperties = new ScannerProperties(ImmutableMap.of("sonar.userHome", symlink.getAbsolutePath()));
+    symlink.toFile().delete();
+    Files.createSymbolicLink(symlink, realSonarHome);
+    when(sonarUserHome.getPath()).thenReturn(symlink);
+
+    ScannerProperties globalProperties = new ScannerProperties(Map.of());
 
-    TempFolder tempFolder = tempFolderProvider.provide(globalProperties);
+    TempFolder tempFolder = underTest.provide(globalProperties, sonarUserHome);
     File newFile = tempFolder.newFile();
-    assertThat(newFile.getParentFile().getParentFile().getAbsolutePath()).isEqualTo(symlink.getAbsolutePath());
+    assertThat(newFile.getParentFile().getParentFile().toPath().toAbsolutePath()).isEqualTo(symlink);
     assertThat(newFile.getParentFile().getName()).startsWith(".sonartmp_");
   }
 
-  private File getCreatedTempDir(File workingDir) {
-    assertThat(workingDir).isDirectory();
-    assertThat(workingDir.listFiles()).hasSize(1);
-    return workingDir.listFiles()[0];
-  }
-
-  private void setFileCreationDate(File f, long time) throws IOException {
-    BasicFileAttributeView attributes = Files.getFileAttributeView(f.toPath(), BasicFileAttributeView.class);
+  private void setFileCreationDate(Path f, long time) throws IOException {
+    BasicFileAttributeView attributes = Files.getFileAttributeView(f, BasicFileAttributeView.class);
     FileTime creationTime = FileTime.fromMillis(time);
     attributes.setTimes(creationTime, creationTime, creationTime);
   }
index 21d6b3c5317a8c1fb4ac54152ea5ff9ecd7e8948..907e13f92bc875e477263e940b997ed724496787 100644 (file)
  */
 package org.sonar.scanner.bootstrap;
 
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.Optional;
-import java.util.concurrent.TimeUnit;
 import javax.annotation.Nullable;
-import okhttp3.HttpUrl;
-import okhttp3.mockwebserver.MockResponse;
-import okhttp3.mockwebserver.MockWebServer;
-import okio.Buffer;
 import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.RandomStringUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.notifications.AnalysisWarnings;
 import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.client.HttpConnector;
 import org.sonarqube.ws.client.WsClientFactories;
 
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.notFound;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.serverError;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
 import static org.apache.commons.io.FileUtils.moveFile;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.ThrowableAssert.ThrowingCallable;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.scanner.bootstrap.PluginFiles.PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY;
 
-public class PluginFilesTest {
+class PluginFilesTest {
 
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
-  @Rule
-  public MockWebServer server = new MockWebServer();
+  @RegisterExtension
+  static WireMockExtension sonarqube = WireMockExtension.newInstance()
+    .options(wireMockConfig().dynamicPort())
+    .build();
+
+  @TempDir
+  private Path tempDir;
+
+  private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
 
   private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
 
-  private File userHome;
   private PluginFiles underTest;
 
-  @Before
-  public void setUp() throws Exception {
-    HttpConnector connector = HttpConnector.newBuilder().acceptGzip(true).url(server.url("/").toString()).build();
+  @BeforeEach
+  void setUp(@TempDir Path sonarUserHomeDir) throws Exception {
+    when(sonarUserHome.getPath()).thenReturn(sonarUserHomeDir);
+
+    HttpConnector connector = HttpConnector.newBuilder().acceptGzip(true).url(sonarqube.url("/")).build();
     GlobalAnalysisMode analysisMode = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
     DefaultScannerWsClient wsClient = new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connector), false,
       analysisMode, analysisWarnings);
 
-    userHome = temp.newFolder();
     MapSettings settings = new MapSettings();
-    settings.setProperty("sonar.userHome", userHome.getAbsolutePath());
     settings.setProperty(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY, 1);
 
-    underTest = new PluginFiles(wsClient, settings.asConfig());
+    underTest = new PluginFiles(wsClient, settings.asConfig(), sonarUserHome);
   }
 
   @Test
-  public void get_jar_from_cache_if_present() throws Exception {
+  void get_jar_from_cache_if_present() throws Exception {
     FileAndMd5 jar = createFileInCache("foo");
 
     File result = underTest.get(newInstalledPlugin("foo", jar.md5)).get();
 
-    verifySameContent(result, jar);
+    verifySameContent(result.toPath(), jar);
     // no requests to server
-    assertThat(server.getRequestCount()).isZero();
+    sonarqube.verify(0, anyRequestedFor(anyUrl()));
   }
 
   @Test
-  public void download_and_add_jar_to_cache_if_missing() throws Exception {
+  void download_and_add_jar_to_cache_if_missing() throws Exception {
     FileAndMd5 tempJar = new FileAndMd5();
-    enqueueDownload(tempJar);
+    stubDownload(tempJar);
 
     InstalledPlugin plugin = newInstalledPlugin("foo", tempJar.md5);
     File result = underTest.get(plugin).get();
 
-    verifySameContent(result, tempJar);
-    HttpUrl requestedUrl = server.takeRequest().getRequestUrl();
-    assertThat(requestedUrl.encodedPath()).isEqualTo("/api/plugins/download");
-    assertThat(requestedUrl.encodedQuery()).isEqualTo("plugin=foo");
+    verifySameContent(result.toPath(), tempJar);
+
+    sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
 
     // get from cache on second call
     result = underTest.get(plugin).get();
-    verifySameContent(result, tempJar);
-    assertThat(server.getRequestCount()).isOne();
+    verifySameContent(result.toPath(), tempJar);
+
+    sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
   }
 
   @Test
-  public void return_empty_if_plugin_not_found_on_server() {
-    server.enqueue(new MockResponse().setResponseCode(404));
+  void return_empty_if_plugin_not_found_on_server() {
+    sonarqube.stubFor(get(anyUrl())
+      .willReturn(notFound()));
 
     InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
     Optional<File> result = underTest.get(plugin);
@@ -119,9 +134,9 @@ public class PluginFilesTest {
   }
 
   @Test
-  public void fail_if_integrity_of_download_is_not_valid() throws IOException {
+  void fail_if_integrity_of_download_is_not_valid() throws IOException {
     FileAndMd5 tempJar = new FileAndMd5();
-    enqueueDownload(tempJar.file, "invalid_hash");
+    stubDownload(tempJar.file, "invalid_hash");
     InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
 
     expectISE("foo", "was expected to have checksum invalid_hash but had " + tempJar.md5,
@@ -129,63 +144,68 @@ public class PluginFilesTest {
   }
 
   @Test
-  public void fail_if_md5_header_is_missing_from_response() throws IOException {
-    File tempJar = temp.newFile();
-    enqueueDownload(tempJar, null);
+  void fail_if_md5_header_is_missing_from_response(@TempDir Path tempDir) throws IOException {
+    var tempJar = Files.createTempFile(tempDir, "plugin", ".jar");
+    stubDownload(tempJar, null);
     InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
 
     expectISE("foo", "did not return header Sonar-MD5", () -> underTest.get(plugin));
   }
 
   @Test
-  public void fail_if_server_returns_error() {
-    server.enqueue(new MockResponse().setResponseCode(500));
+  void fail_if_server_returns_error() {
+    sonarqube.stubFor(get(anyUrl())
+      .willReturn(serverError()));
+
     InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
 
     expectISE("foo", "returned code 500", () -> underTest.get(plugin));
   }
 
   @Test
-  public void getPlugin_whenTimeOutReached_thenDownloadFails() {
-    MockResponse response = new MockResponse().setBody("test").setBodyDelay(2, TimeUnit.SECONDS);
-    response.setHeader("Sonar-MD5", "md5");
-    server.enqueue(response);
+  void getPlugin_whenTimeOutReached_thenDownloadFails() {
+    sonarqube.stubFor(get(anyUrl())
+      .willReturn(ok()
+        .withFixedDelay(5000)));
+
     InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
 
     assertThatThrownBy(() -> underTest.get(plugin))
       .isInstanceOf(IllegalStateException.class)
-      .hasMessageStartingWith("Fail to download plugin [" + plugin.key + "]")
+      .hasMessageStartingWith("Fail to request url")
       .cause().isInstanceOf(SocketTimeoutException.class);
   }
 
   @Test
-  public void download_a_new_version_of_plugin_during_blue_green_switch() throws IOException {
+  void download_a_new_version_of_plugin_during_blue_green_switch() throws IOException {
     FileAndMd5 tempJar = new FileAndMd5();
-    enqueueDownload(tempJar);
+    stubDownload(tempJar);
 
     // expecting to download plugin foo with checksum "abc"
     InstalledPlugin pluginV1 = newInstalledPlugin("foo", "abc");
 
     File result = underTest.get(pluginV1).get();
-    verifySameContent(result, tempJar);
+    verifySameContent(result.toPath(), tempJar);
 
     // new version of downloaded jar is put in cache with the new md5
     InstalledPlugin pluginV2 = newInstalledPlugin("foo", tempJar.md5);
     result = underTest.get(pluginV2).get();
-    verifySameContent(result, tempJar);
-    assertThat(server.getRequestCount()).isOne();
+    verifySameContent(result.toPath(), tempJar);
+
+    sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
 
     // v1 still requests server and downloads v2
-    enqueueDownload(tempJar);
+    stubDownload(tempJar);
     result = underTest.get(pluginV1).get();
-    verifySameContent(result, tempJar);
-    assertThat(server.getRequestCount()).isEqualTo(2);
+    verifySameContent(result.toPath(), tempJar);
+
+    sonarqube.verify(exactly(2), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
   }
 
   @Test
-  public void fail_if_cached_file_is_outside_cache_dir() throws IOException {
+  void fail_if_cached_file_is_outside_cache_dir() throws IOException {
     FileAndMd5 tempJar = new FileAndMd5();
-    enqueueDownload(tempJar);
+    stubDownload(tempJar);
 
     InstalledPlugin plugin = newInstalledPlugin("foo/bar", "abc");
 
@@ -200,29 +220,29 @@ public class PluginFilesTest {
   }
 
   private FileAndMd5 moveToCache(String pluginKey, FileAndMd5 jar) throws IOException {
-    File jarInCache = new File(userHome, "cache/" + jar.md5 + "/sonar-" + pluginKey + "-plugin.jar");
-    moveFile(jar.file, jarInCache);
+    Path jarInCache = sonarUserHome.getPath().resolve("cache/" + jar.md5 + "/sonar-" + pluginKey + "-plugin.jar");
+    moveFile(jar.file.toFile(), jarInCache.toFile());
     return new FileAndMd5(jarInCache, jar.md5);
   }
 
   /**
    * Enqueue download of file with valid MD5
    */
-  private void enqueueDownload(FileAndMd5 file) throws IOException {
-    enqueueDownload(file.file, file.md5);
+  private void stubDownload(FileAndMd5 file) throws IOException {
+    stubDownload(file.file, file.md5);
   }
 
   /**
    * Enqueue download of file with a MD5 that may not be returned (null) or not valid
    */
-  private void enqueueDownload(File file, @Nullable String md5) throws IOException {
-    Buffer body = new Buffer();
-    body.write(FileUtils.readFileToByteArray(file));
-    MockResponse response = new MockResponse().setBody(body);
+  private void stubDownload(Path file, @Nullable String md5) throws IOException {
+    var responseDefBuilder = ok();
     if (md5 != null) {
-      response.setHeader("Sonar-MD5", md5);
+      responseDefBuilder.withHeader("Sonar-MD5", md5);
     }
-    server.enqueue(response);
+    responseDefBuilder.withBody(Files.readAllBytes(file));
+    sonarqube.stubFor(get(urlMatching("/api/plugins/download\\?plugin=.*"))
+      .willReturn(responseDefBuilder));
   }
 
   private static InstalledPlugin newInstalledPlugin(String pluginKey, String fileChecksum) {
@@ -232,10 +252,10 @@ public class PluginFilesTest {
     return plugin;
   }
 
-  private static void verifySameContent(File file1, FileAndMd5 file2) {
-    assertThat(file1).isFile().exists();
-    assertThat(file2.file).isFile().exists();
-    assertThat(file1).hasSameContentAs(file2.file);
+  private static void verifySameContent(Path file1, FileAndMd5 file2) {
+    assertThat(file1).isRegularFile();
+    assertThat(file2.file).isRegularFile();
+    assertThat(file1).hasSameTextualContentAs(file2.file);
   }
 
   private void expectISE(String pluginKey, String message, ThrowingCallable shouldRaiseThrowable) {
@@ -246,18 +266,18 @@ public class PluginFilesTest {
   }
 
   private class FileAndMd5 {
-    private final File file;
+    private final Path file;
     private final String md5;
 
-    FileAndMd5(File file, String md5) {
+    FileAndMd5(Path file, String md5) {
       this.file = file;
       this.md5 = md5;
     }
 
     FileAndMd5() throws IOException {
-      this.file = temp.newFile();
-      FileUtils.write(this.file, RandomStringUtils.random(3));
-      try (InputStream fis = FileUtils.openInputStream(this.file)) {
+      this.file = Files.createTempFile(tempDir, "jar", null);
+      Files.write(this.file, RandomStringUtils.random(3).getBytes());
+      try (InputStream fis = Files.newInputStream(this.file)) {
         this.md5 = DigestUtils.md5Hex(fis);
       } catch (IOException e) {
         throw new IllegalStateException("Fail to compute md5 of " + this.file, e);
index 0d27bcb284b5e86f389af3f75e0362531e9891c5..210c5228bd9bde5edbfefd8e6f47c46d7393f027 100644 (file)
@@ -35,6 +35,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.scanner.WsTestUtil;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/ScannerWsClientProviderTest.java
deleted file mode 100644 (file)
index 2e63d4c..0000000
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.bootstrap;
-
-import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInfo;
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.sonar.api.notifications.AnalysisWarnings;
-import org.sonar.api.utils.System2;
-import org.sonar.batch.bootstrapper.EnvironmentInformation;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.HttpConnector;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
-import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-class ScannerWsClientProviderTest {
-
-  public static final GlobalAnalysisMode GLOBAL_ANALYSIS_MODE = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
-  public static final AnalysisWarnings ANALYSIS_WARNINGS = warning -> {
-  };
-  private final Map<String, String> scannerProps = new HashMap<>();
-
-  private final ScannerWsClientProvider underTest = new ScannerWsClientProvider();
-  private final EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
-  public static final String PROXY_AUTH_ENABLED = "proxy-auth";
-
-  @RegisterExtension
-  static WireMockExtension sonarqubeMock = WireMockExtension.newInstance()
-    .options(wireMockConfig().dynamicPort())
-    .build();
-
-  @RegisterExtension
-  static WireMockExtension proxyMock = WireMockExtension.newInstance()
-    .options(wireMockConfig().dynamicPort())
-    .build();
-
-  private final System2 system2 = mock(System2.class);
-  private final Properties systemProps = new Properties();
-
-  @BeforeEach
-  void configureMocks(TestInfo info) {
-    when(system2.properties()).thenReturn(systemProps);
-
-    if (info.getTags().contains(PROXY_AUTH_ENABLED)) {
-      proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
-        .inScenario("Proxy Auth")
-        .whenScenarioStateIs(STARTED)
-        .willReturn(aResponse()
-          .withStatus(407)
-          .withHeader("Proxy-Authenticate", "Basic realm=\"Access to the proxy\""))
-        .willSetStateTo("Challenge returned"));
-      proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
-        .inScenario("Proxy Auth")
-        .whenScenarioStateIs("Challenge returned")
-        .willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
-    } else {
-      proxyMock.stubFor(get(urlMatching("/api/plugins/.*")).willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
-    }
-  }
-
-  @Test
-  void provide_client_with_default_settings() {
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    assertThat(client).isNotNull();
-    assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/");
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-    assertThat(httpConnector.baseUrl()).isEqualTo("http://localhost:9000/");
-    assertThat(httpConnector.okHttpClient().proxy()).isNull();
-    assertThat(httpConnector.okHttpClient().connectTimeoutMillis()).isEqualTo(5_000);
-    assertThat(httpConnector.okHttpClient().readTimeoutMillis()).isEqualTo(60_000);
-
-    // Proxy is not accessed
-    assertThat(proxyMock.findAllUnmatchedRequests()).isEmpty();
-  }
-
-  @Test
-  void provide_client_with_custom_settings() {
-    scannerProps.put("sonar.host.url", "https://here/sonarqube");
-    scannerProps.put("sonar.token", "testToken");
-    scannerProps.put("sonar.ws.timeout", "42");
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    assertThat(client).isNotNull();
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-    assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/");
-    assertThat(httpConnector.okHttpClient().proxy()).isNull();
-  }
-
-  @Test
-  void it_should_timeout_on_long_response() {
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.responseTimeout", "PT0.2S");
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    sonarqubeMock.stubFor(get("/api/plugins/installed")
-      .willReturn(aResponse().withStatus(200)
-        .withFixedDelay(2000)
-        .withBody("Success")));
-
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-
-    var getRequest = new GetRequest("api/plugins/installed");
-    var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
-
-    assertThat(thrown).hasStackTraceContaining("timeout");
-  }
-
-  @Test
-  void it_should_timeout_on_slow_response() {
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.socketTimeout", "PT0.2S");
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    sonarqubeMock.stubFor(get("/api/plugins/installed")
-      .willReturn(aResponse().withStatus(200)
-        .withChunkedDribbleDelay(2, 2000)
-        .withBody("Success")));
-
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-
-    var getRequest = new GetRequest("api/plugins/installed");
-    var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
-
-    assertThat(thrown).hasStackTraceContaining("timeout");
-  }
-
-  @Test
-  void it_should_honor_scanner_proxy_settings() {
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.proxyHost", "localhost");
-    scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    sonarqubeMock.stubFor(get("/api/plugins/installed")
-      .willReturn(aResponse().withStatus(200).withBody("Success")));
-
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-    try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
-      assertThat(r.code()).isEqualTo(200);
-      assertThat(r.content()).isEqualTo("Success");
-    }
-
-    proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed")));
-  }
-
-  @Test
-  void it_should_throw_if_invalid_proxy_port() {
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.proxyHost", "localhost");
-    scannerProps.put("sonar.scanner.proxyPort", "not_a_number");
-    var scannerPropertiesBean = new ScannerProperties(scannerProps);
-
-    assertThrows(IllegalArgumentException.class, () -> underTest.provide(scannerPropertiesBean, env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS));
-  }
-
-  @Test
-  @Tag(PROXY_AUTH_ENABLED)
-  void it_should_honor_scanner_proxy_settings_with_auth() {
-    var proxyLogin = "proxyLogin";
-    var proxyPassword = "proxyPassword";
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.proxyHost", "localhost");
-    scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
-    scannerProps.put("sonar.scanner.proxyUser", proxyLogin);
-    scannerProps.put("sonar.scanner.proxyPassword", proxyPassword);
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    sonarqubeMock.stubFor(get("/api/plugins/installed")
-      .willReturn(aResponse().withStatus(200).withBody("Success")));
-
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-    try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
-      assertThat(r.code()).isEqualTo(200);
-      assertThat(r.content()).isEqualTo("Success");
-    }
-
-    proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
-      .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
-
-  }
-
-  @Test
-  @Tag(PROXY_AUTH_ENABLED)
-  void it_should_honor_old_jvm_proxy_auth_properties() {
-    var proxyLogin = "proxyLogin";
-    var proxyPassword = "proxyPassword";
-    scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
-    scannerProps.put("sonar.scanner.proxyHost", "localhost");
-    scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
-    systemProps.put("http.proxyUser", proxyLogin);
-    systemProps.put("http.proxyPassword", proxyPassword);
-
-    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS);
-
-    sonarqubeMock.stubFor(get("/api/plugins/installed")
-      .willReturn(aResponse().withStatus(200).withBody("Success")));
-
-    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
-    try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
-      assertThat(r.code()).isEqualTo(200);
-      assertThat(r.content()).isEqualTo("Success");
-    }
-
-    proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
-      .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
-
-  }
-}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/SonarUserHomeProviderTest.java
new file mode 100644 (file)
index 0000000..e499029
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.bootstrap;
+
+import java.nio.file.Path;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.sonar.api.utils.System2;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SonarUserHomeProviderTest {
+
+  private final System2 system = mock(System2.class);
+  private final SonarUserHomeProvider underTest = new SonarUserHomeProvider(system);
+
+  @Test
+  void createTempFolderFromDefaultUserHome(@TempDir Path userHome) {
+    when(system.envVariable("SONAR_USER_HOME")).thenReturn(null);
+    when(system.property("user.home")).thenReturn(userHome.toString());
+
+    var sonarUserHome = underTest.provide(new ScannerProperties(Map.of()));
+
+    assertThat(sonarUserHome.getPath()).isEqualTo(userHome.resolve(".sonar"));
+  }
+
+  @Test
+  void should_consider_env_variable_over_user_home(@TempDir Path userHome, @TempDir Path sonarUserHomeFromEnv) {
+    when(system.envVariable("SONAR_USER_HOME")).thenReturn(sonarUserHomeFromEnv.toString());
+    when(system.property("user.home")).thenReturn(userHome.toString());
+
+    var sonarUserHome = underTest.provide(new ScannerProperties(Map.of()));
+
+    assertThat(sonarUserHome.getPath()).isEqualTo(sonarUserHomeFromEnv);
+  }
+
+  @Test
+  void should_consider_scanner_property_over_env_and_user_home(@TempDir Path userHome, @TempDir Path sonarUserHomeFromEnv, @TempDir Path sonarUserHomeFromProps) {
+    when(system.envVariable("SONAR_USER_HOME")).thenReturn(sonarUserHomeFromEnv.toString());
+    when(system.property("user.home")).thenReturn(userHome.toString());
+
+    var sonarUserHome = underTest.provide(new ScannerProperties(Map.of("sonar.userHome", sonarUserHomeFromProps.toString())));
+
+    assertThat(sonarUserHome.getPath()).isEqualTo(sonarUserHomeFromProps);
+  }
+
+}
index 568c6d00aac5992161fc9f4823e743f0b9060b9a..aaf4783ac61031a7569edb975f39d499926ccdce 100644 (file)
@@ -34,7 +34,7 @@ import org.mockito.ArgumentCaptor;
 import org.sonar.api.scanner.fs.InputProject;
 import org.sonar.api.utils.MessageException;
 import org.sonar.api.testfixtures.log.LogTester;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.protocol.internal.ScannerInternal.SensorCacheEntry;
 import org.sonar.scanner.protocol.internal.SensorCacheData;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/DefaultScannerWsClientTest.java
new file mode 100644 (file)
index 0000000..95f98c4
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * 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");
+  }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/http/ScannerWsClientProviderTest.java
new file mode 100644 (file)
index 0000000..e0ee312
--- /dev/null
@@ -0,0 +1,413 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.http;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.RestoreSystemProperties;
+import org.sonar.api.notifications.AnalysisWarnings;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
+import org.sonar.scanner.bootstrap.ScannerProperties;
+import org.sonar.scanner.bootstrap.SonarUserHome;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ScannerWsClientProviderTest {
+
+  private static final GlobalAnalysisMode GLOBAL_ANALYSIS_MODE = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
+  private static final AnalysisWarnings ANALYSIS_WARNINGS = warning -> {
+  };
+  @TempDir
+  private Path sonarUserHomeDir;
+  private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
+  private final Map<String, String> scannerProps = new HashMap<>();
+
+  private final ScannerWsClientProvider underTest = new ScannerWsClientProvider();
+  private final EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
+
+  private final System2 system2 = mock(System2.class);
+  private final Properties systemProps = new Properties();
+
+  @BeforeEach
+  void configureMocks() {
+    when(system2.properties()).thenReturn(systemProps);
+    when(sonarUserHome.getPath()).thenReturn(sonarUserHomeDir);
+  }
+
+  @Test
+  void provide_client_with_default_settings() {
+    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+    assertThat(client).isNotNull();
+    assertThat(client.baseUrl()).isEqualTo("http://localhost:9000/");
+    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+    assertThat(httpConnector.baseUrl()).isEqualTo("http://localhost:9000/");
+    assertThat(httpConnector.okHttpClient().proxy()).isNull();
+    assertThat(httpConnector.okHttpClient().connectTimeoutMillis()).isEqualTo(5_000);
+    assertThat(httpConnector.okHttpClient().readTimeoutMillis()).isEqualTo(60_000);
+  }
+
+  @Test
+  void provide_client_with_custom_settings() {
+    scannerProps.put("sonar.host.url", "https://here/sonarqube");
+    scannerProps.put("sonar.token", "testToken");
+    scannerProps.put("sonar.ws.timeout", "42");
+
+    DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+    assertThat(client).isNotNull();
+    HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+    assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/");
+    assertThat(httpConnector.okHttpClient().proxy()).isNull();
+  }
+
+  @Nested
+  class WithMockHttpSonarQube {
+
+    @RegisterExtension
+    static WireMockExtension sonarqubeMock = WireMockExtension.newInstance()
+      .options(wireMockConfig().dynamicPort())
+      .build();
+
+    @Test
+    void it_should_timeout_on_long_response() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      scannerProps.put("sonar.scanner.responseTimeout", "PT0.2S");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      sonarqubeMock.stubFor(get("/api/plugins/installed")
+        .willReturn(aResponse().withStatus(200)
+          .withFixedDelay(2000)
+          .withBody("Success")));
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+
+      var getRequest = new GetRequest("api/plugins/installed");
+      var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
+
+      assertThat(thrown).hasStackTraceContaining("timeout");
+    }
+
+    @Test
+    void it_should_timeout_on_slow_response() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      scannerProps.put("sonar.scanner.socketTimeout", "PT0.2S");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      sonarqubeMock.stubFor(get("/api/plugins/installed")
+        .willReturn(aResponse().withStatus(200)
+          .withChunkedDribbleDelay(2, 2000)
+          .withBody("Success")));
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+
+      var getRequest = new GetRequest("api/plugins/installed");
+      var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
+
+      assertThat(thrown).hasStackTraceContaining("timeout");
+    }
+
+    @Test
+    void it_should_throw_if_invalid_proxy_port() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      scannerProps.put("sonar.scanner.proxyHost", "localhost");
+      scannerProps.put("sonar.scanner.proxyPort", "not_a_number");
+      var scannerPropertiesBean = new ScannerProperties(scannerProps);
+
+      assertThrows(IllegalArgumentException.class, () -> underTest.provide(scannerPropertiesBean, env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome));
+    }
+
+    @Nested
+    class WithProxy {
+
+      private static final String PROXY_AUTH_ENABLED = "proxy-auth";
+
+      @RegisterExtension
+      static WireMockExtension proxyMock = WireMockExtension.newInstance()
+        .options(wireMockConfig().dynamicPort())
+        .build();
+
+      @BeforeEach
+      void configureMocks(TestInfo info) {
+        if (info.getTags().contains(PROXY_AUTH_ENABLED)) {
+          proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
+            .inScenario("Proxy Auth")
+            .whenScenarioStateIs(STARTED)
+            .willReturn(aResponse()
+              .withStatus(407)
+              .withHeader("Proxy-Authenticate", "Basic realm=\"Access to the proxy\""))
+            .willSetStateTo("Challenge returned"));
+          proxyMock.stubFor(get(urlMatching("/api/plugins/.*"))
+            .inScenario("Proxy Auth")
+            .whenScenarioStateIs("Challenge returned")
+            .willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
+        } else {
+          proxyMock.stubFor(get(urlMatching("/api/plugins/.*")).willReturn(aResponse().proxiedFrom(sonarqubeMock.baseUrl())));
+        }
+      }
+
+      @Test
+      void it_should_honor_scanner_proxy_settings() {
+        scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+        scannerProps.put("sonar.scanner.proxyHost", "localhost");
+        scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
+
+        DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+        sonarqubeMock.stubFor(get("/api/plugins/installed")
+          .willReturn(aResponse().withStatus(200).withBody("Success")));
+
+        HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+        try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+          assertThat(r.code()).isEqualTo(200);
+          assertThat(r.content()).isEqualTo("Success");
+        }
+
+        proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed")));
+      }
+
+      @Test
+      @Tag(PROXY_AUTH_ENABLED)
+      void it_should_honor_scanner_proxy_settings_with_auth() {
+        var proxyLogin = "proxyLogin";
+        var proxyPassword = "proxyPassword";
+        scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+        scannerProps.put("sonar.scanner.proxyHost", "localhost");
+        scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
+        scannerProps.put("sonar.scanner.proxyUser", proxyLogin);
+        scannerProps.put("sonar.scanner.proxyPassword", proxyPassword);
+
+        DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+        sonarqubeMock.stubFor(get("/api/plugins/installed")
+          .willReturn(aResponse().withStatus(200).withBody("Success")));
+
+        HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+        try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+          assertThat(r.code()).isEqualTo(200);
+          assertThat(r.content()).isEqualTo("Success");
+        }
+
+        proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
+          .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
+
+      }
+
+      @Test
+      @Tag(PROXY_AUTH_ENABLED)
+      void it_should_honor_old_jvm_proxy_auth_properties() {
+        var proxyLogin = "proxyLogin";
+        var proxyPassword = "proxyPassword";
+        scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+        scannerProps.put("sonar.scanner.proxyHost", "localhost");
+        scannerProps.put("sonar.scanner.proxyPort", "" + proxyMock.getPort());
+        systemProps.put("http.proxyUser", proxyLogin);
+        systemProps.put("http.proxyPassword", proxyPassword);
+
+        DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+        sonarqubeMock.stubFor(get("/api/plugins/installed")
+          .willReturn(aResponse().withStatus(200).withBody("Success")));
+
+        HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+        try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+          assertThat(r.code()).isEqualTo(200);
+          assertThat(r.content()).isEqualTo("Success");
+        }
+
+        proxyMock.verify(getRequestedFor(urlEqualTo("/api/plugins/installed"))
+          .withHeader("Proxy-Authorization", equalTo("Basic " + Base64.getEncoder().encodeToString((proxyLogin + ":" + proxyPassword).getBytes(StandardCharsets.UTF_8)))));
+
+      }
+    }
+
+  }
+
+  @Nested
+  class WithMockHttpsSonarQube {
+
+    public static final String KEYSTORE_PWD = "pwdServerP12";
+
+    @RegisterExtension
+    static WireMockExtension sonarqubeMock = WireMockExtension.newInstance()
+      .options(wireMockConfig().dynamicHttpsPort().httpDisabled(true)
+        .keystoreType("pkcs12")
+        .keystorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server.p12"))).toString())
+        .keystorePassword(KEYSTORE_PWD)
+        .keyManagerPassword(KEYSTORE_PWD))
+      .build();
+
+    @BeforeEach
+    void mockResponse() {
+      sonarqubeMock.stubFor(get("/api/plugins/installed")
+        .willReturn(aResponse().withStatus(200).withBody("Success")));
+    }
+
+    @Test
+    void it_should_not_trust_server_self_signed_certificate_by_default() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+      var getRequest = new GetRequest("api/plugins/installed");
+      var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
+
+      assertThat(thrown).hasStackTraceContaining("CertificateException");
+    }
+
+    @Test
+    void it_should_trust_server_self_signed_certificate_when_certificate_is_in_truststore() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString());
+      scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+      try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+        assertThat(r.code()).isEqualTo(200);
+        assertThat(r.content()).isEqualTo("Success");
+      }
+    }
+  }
+
+  @Nested
+  class WithMockHttpsSonarQubeAndClientCertificates {
+
+    public static final String KEYSTORE_PWD = "pwdServerP12";
+
+    @RegisterExtension
+    static WireMockExtension sonarqubeMock = WireMockExtension.newInstance()
+      .options(wireMockConfig().dynamicHttpsPort().httpDisabled(true)
+        .keystoreType("pkcs12")
+        .keystorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server.p12"))).toString())
+        .keystorePassword(KEYSTORE_PWD)
+        .keyManagerPassword(KEYSTORE_PWD)
+        .needClientAuth(true)
+        .trustStoreType("pkcs12")
+        .trustStorePath(toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/server-with-client-ca.p12"))).toString())
+        .trustStorePassword("pwdServerWithClientCA"))
+      .build();
+
+    @BeforeEach
+    void mockResponse() {
+      sonarqubeMock.stubFor(get("/api/plugins/installed")
+        .willReturn(aResponse().withStatus(200).withBody("Success")));
+    }
+
+    @Test
+    void it_should_fail_if_client_certificate_not_provided() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString());
+      scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+      var getRequest = new GetRequest("api/plugins/installed");
+      var thrown = assertThrows(IllegalStateException.class, () -> httpConnector.call(getRequest));
+
+      assertThat(thrown).satisfiesAnyOf(
+        e -> assertThat(e).hasStackTraceContaining("SSLHandshakeException"),
+        // Exception is flaky because of https://bugs.openjdk.org/browse/JDK-8172163
+        e -> assertThat(e).hasStackTraceContaining("Broken pipe"));
+    }
+
+    @Test
+    void it_should_authenticate_using_certificate_in_keystore() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+
+      scannerProps.put("sonar.scanner.truststorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString());
+      scannerProps.put("sonar.scanner.truststorePassword", "pwdClientWithServerCA");
+      scannerProps.put("sonar.scanner.keystorePath", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client.p12"))).toString());
+      scannerProps.put("sonar.scanner.keystorePassword", "pwdClientCertP12");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+      try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+        assertThat(r.code()).isEqualTo(200);
+        assertThat(r.content()).isEqualTo("Success");
+      }
+    }
+
+    @RestoreSystemProperties
+    @Test
+    void it_should_support_jvm_system_properties() {
+      scannerProps.put("sonar.host.url", sonarqubeMock.baseUrl());
+      System.setProperty("javax.net.ssl.trustStore", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client-truststore.p12"))).toString());
+      System.setProperty("javax.net.ssl.trustStorePassword", "pwdClientWithServerCA");
+      System.setProperty("javax.net.ssl.keyStore", toPath(requireNonNull(ScannerWsClientProviderTest.class.getResource("/ssl/client.p12"))).toString());
+      systemProps.setProperty("javax.net.ssl.keyStore", "any value is fine here");
+      System.setProperty("javax.net.ssl.keyStorePassword", "pwdClientCertP12");
+
+      DefaultScannerWsClient client = underTest.provide(new ScannerProperties(scannerProps), env, GLOBAL_ANALYSIS_MODE, system2, ANALYSIS_WARNINGS, sonarUserHome);
+
+      HttpConnector httpConnector = (HttpConnector) client.wsConnector();
+      try (var r = httpConnector.call(new GetRequest("api/plugins/installed"))) {
+        assertThat(r.code()).isEqualTo(200);
+        assertThat(r.content()).isEqualTo("Success");
+      }
+    }
+  }
+
+  private static Path toPath(URL url) {
+    try {
+      return Paths.get(url.toURI());
+    } catch (URISyntaxException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
index 0a4b1adc125eb0b7372f67d00b2434bac3710ad4..b9bb8f112b8e3056913996411b47b00d2f615dcc 100644 (file)
@@ -24,7 +24,7 @@ import org.sonar.api.CoreProperties;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.Version;
 import org.sonar.core.platform.SonarQubeVersion;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
index 89013ebcbc9cf2cc008bf6e75a07d4b02f792e56..0e7e67cb5c5560d4e5fc0a7bbd2aa7722fa016df 100644 (file)
@@ -33,7 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.slf4j.event.Level;
 import org.sonar.api.testfixtures.log.LogTesterJUnit5;
 import org.sonar.api.utils.MessageException;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
 import org.sonar.scanner.report.CeTaskReportDataHolder;
 import org.sonar.scanner.scan.ScanProperties;
index 58a6f9dd9cdf3b11514b545ef3ce0fc9d6bf5016..9d05effedbf44c449520f7e58d634c365eaf6815 100644 (file)
@@ -39,7 +39,7 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.testfixtures.log.LogTester;
 import org.sonar.api.utils.MessageException;
 import org.sonar.api.utils.TempFolder;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.GlobalAnalysisMode;
 import org.sonar.scanner.ci.CiConfiguration;
 import org.sonar.scanner.ci.DevOpsPlatformInfo;
index 128e4ffb23a43424aff5dd648c527187b41a38f1..f0b61cf71dd4b0ea35211d5506ede40395cc21ea 100644 (file)
@@ -26,7 +26,7 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
index 2fe7f09ae8496f877b87a338d237f54f0bfdc1f2..856bb82823a79cb3698907bd850a1fa73abe8acd 100644 (file)
@@ -25,7 +25,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import org.junit.Test;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.NewCodePeriods;
 
 import static org.mockito.Mockito.mock;
index cf0391cc770f088c54f2d3e381e95acc40adbde1..e55fb9880d021e065516a95ac393170d3fc4a1e8 100644 (file)
@@ -30,7 +30,7 @@ import org.junit.Test;
 import org.sonar.api.batch.fs.internal.DefaultInputFile;
 import org.sonar.api.utils.MessageException;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Batch.WsProjectResponse;
 import org.sonarqube.ws.client.HttpException;
 
index fac637b5860c7f1abbebb9b6bf0a2e5c7269087e..8c0253fbe9d70cf1f4ab488ba4c85f11b62c138f 100644 (file)
@@ -26,7 +26,7 @@ import java.io.InputStream;
 import org.junit.Test;
 import org.sonar.api.utils.MessageException;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Qualityprofiles;
 import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
 import org.sonarqube.ws.client.HttpException;
index 6125ce990767570bfc0e79547359f79507ede516..294e4763d2eccab2f908a5fd5bf37f44e3d656a0 100644 (file)
@@ -28,7 +28,7 @@ import org.slf4j.event.Level;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.testfixtures.log.LogTester;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;
index 0e6e74013d8abd5d22b2a62e741403a23d720bdd..0b3d1bb23b148e95f6f52cb1ced216938c2bd5fb 100644 (file)
@@ -25,7 +25,7 @@ import java.io.PipedOutputStream;
 import java.util.Map;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonarqube.ws.Settings;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.WsResponse;
index 417be96ba99bb67e13cdc441d2d792b611e539de..c1694faa9a13056cf625a339ea70fd065a2be62b 100644 (file)
@@ -25,7 +25,7 @@ import java.io.PipedOutputStream;
 import java.util.Map;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.bootstrap.ScannerProperties;
 import org.sonarqube.ws.Settings;
 import org.sonarqube.ws.client.GetRequest;
index 1ba8fd8d93a4df97385454c4b330fc59054a73aa..bae17ee866ddf798d3355e4a0a40ff17c947037f 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.api.batch.rule.LoadedActiveRule;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
 import org.sonar.scanner.WsTestUtil;
-import org.sonar.scanner.bootstrap.DefaultScannerWsClient;
+import org.sonar.scanner.http.DefaultScannerWsClient;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Rules;
diff --git a/sonar-scanner-engine/src/test/resources/ssl/README.md b/sonar-scanner-engine/src/test/resources/ssl/README.md
new file mode 100644 (file)
index 0000000..140d49a
--- /dev/null
@@ -0,0 +1,452 @@
+## Let's create TLS certificates
+
+The most common format of certificates are PEM, so let's generate them instead of using Java keytool (that can also generate keys in JKS format).
+
+This README, is a *simplified* version for generating the certificates only for test's purposes.
+
+**DO NOT USE IT FOR PRODUCTION**
+
+### Generation of a TLS server certificate
+
+In this example the configuration of OpenSSL is entirely in openssl.conf (a stripped version of openssl.cnf that may vary from distribution to distribution)
+
+#### First let's create a Certificate Authority
+
+The Certificate Authority is a private key that is used to sign other X.509 certificates in order to validate the ownership of a website (trusted tier).
+
+```bash
+$ openssl genrsa -out ca.key 4096
+.....++
+................................................................................................................................................++
+e is 65537 (0x010001)
+```
+
+Now we have our key to sign other certificates : `ca.key` in PEM format.
+
+Let's create our X.509 CA certificate :
+
+```bash
+$ openssl req -key ca.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca.crt -config ./openssl.conf
+You are about to be asked to enter information that will be incorporated
+into your certificate request.
+What you are about to enter is what is called a Distinguished Name or a DN.
+There are quite a few fields but you can leave some blank
+For some fields there will be a default value,
+If you enter '.', the field will be left blank.
+-----
+Country Name (2-letter code) [CH]:
+State or Province Name (full name) [Geneva]:
+Locality (e.g. city name) [Geneva]:
+Organization (e.g. company name) [SonarSource SA]:
+Common Name (your.domain.com) [localhost]:
+```
+
+There is no important values here.
+
+#### Let's create a self-signed certificate our TLS server using our CA
+
+We want to create a X.509 certificate for our https server. This certificate will be a Certificate Signing Request. A certificate that need to be signed by a trusted tier.
+The default configuration is set in `openssl.conf` and it has been configuration for `localhost`.
+The most important part is the `Common Name` and `DNS.1` (set in `openssl.conf`).
+
+So just keep using enter with this command line :
+
+```bash
+$ openssl req -new -keyout server.key -out server.csr -nodes -newkey rsa:4096 -config ./openssl.conf
+  Generating a 4096 bit RSA private key
+  ........................................................................++
+  .........................................................................................++
+  writing new private key to 'server.key'
+  -----
+  You are about to be asked to enter information that will be incorporated
+  into your certificate request.
+  What you are about to enter is what is called a Distinguished Name or a DN.
+  There are quite a few fields but you can leave some blank
+  For some fields there will be a default value,
+  If you enter '.', the field will be left blank.
+  -----
+  Country Name (2-letter code) [CH]:
+  State or Province Name (full name) [Geneva]:
+  Locality (e.g. city name) [Geneva]:
+  Organization (e.g. company name) [SonarSource SA]:
+  Common Name (your.domain.com) [localhost]:
+```
+
+No we have `server.csr` file valid for 10 years.
+Let's see what's in this certificate :
+```bash
+$ openssl req -verify -in server.csr -text -noout
+verify OK
+Certificate Request:
+    Data:
+        Version: 1 (0x0)
+        Subject: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (4096 bit)
+                Modulus:
+                    00:c8:2d:dc:64:1a:b6:d9:a9:3e:bd:3f:d3:ae:27:
+                    ab:00:a8:09:f7:9e:ae:b5:70:c0:11:ab:2d:45:48:
+                    6c:b9:b3:b1:4b:42:b7:4e:48:d3:2e:38:cb:e5:7d:
+                    14:30:d3:b8:1d:2f:e2:09:04:cc:aa:80:09:51:bc:
+                    59:9d:a7:7a:76:34:cc:7a:2b:ae:d3:ef:98:38:ef:
+                    b2:8a:0e:e9:2f:79:4e:d4:a9:10:63:2b:5b:05:05:
+                    ef:6b:98:41:e3:c0:3e:6c:5f:8a:66:10:ca:98:e5:
+                    37:c6:ea:13:48:c9:92:22:53:44:1a:61:27:f4:60:
+                    16:a7:a9:87:a9:d3:cf:88:5e:d4:47:44:24:4f:6d:
+                    5e:c0:4a:ff:ad:e4:82:63:da:82:eb:9e:b3:76:6f:
+                    5d:b4:2d:fc:96:4a:98:e4:f5:20:97:48:38:11:29:
+                    33:7d:5a:96:fa:28:49:9f:cb:24:f8:02:f6:bb:ed:
+                    f3:91:90:51:10:c2:93:28:56:6e:4d:51:51:10:27:
+                    8f:c3:f0:cd:ee:51:2d:dc:e5:a7:21:55:20:44:ac:
+                    8b:66:1d:b7:eb:e0:ed:69:f0:d4:32:82:ee:53:91:
+                    3b:ee:58:83:ba:3b:9d:3f:f7:23:0e:36:46:20:6b:
+                    6a:80:9b:11:46:28:39:60:25:69:9e:e5:d0:34:ba:
+                    2b:c3:33:f2:44:3d:fb:8f:2d:47:a6:ae:64:9a:b3:
+                    5a:f0:ed:cb:3e:86:33:80:23:32:d0:e7:51:91:a8:
+                    c6:97:d1:7c:e4:02:52:5d:7c:a9:97:83:00:c5:10:
+                    fb:13:f9:29:1f:79:c4:a5:8c:7b:64:e0:cd:b6:a1:
+                    34:36:aa:f4:63:63:77:12:d3:fa:fe:1d:54:2e:64:
+                    43:38:a2:71:28:72:7a:bf:33:cb:8c:27:a7:66:51:
+                    8f:6f:e8:d2:90:19:2f:d4:8e:ac:b4:7b:e0:53:a8:
+                    0f:11:d1:7d:08:71:de:0a:a4:63:10:79:c8:e8:bf:
+                    7e:be:8b:06:7d:43:9b:4b:a1:0a:49:a6:c8:c6:43:
+                    c4:24:23:13:2a:b2:f9:f2:b8:e7:8e:ab:3e:2a:b5:
+                    50:26:23:d6:b2:d3:ee:23:ec:d1:36:92:70:2e:df:
+                    82:6a:d2:07:bb:f0:97:51:42:e4:d8:49:69:35:bb:
+                    38:90:1f:8e:aa:1d:27:78:26:26:d4:36:75:ee:83:
+                    17:69:cb:7f:53:45:8f:b4:63:13:d5:fd:42:10:8a:
+                    d3:75:38:4a:bd:13:cf:68:5e:41:6d:f0:57:b5:75:
+                    e3:dc:10:82:ab:29:ed:a1:27:9c:50:74:f2:4c:4a:
+                    a3:78:2a:53:ca:90:a6:89:20:24:85:b5:ec:c9:c7:
+                    be:96:b5
+                Exponent: 65537 (0x10001)
+        Attributes:
+        Requested Extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:localhost
+            X509v3 Key Usage: 
+                Digital Signature, Key Encipherment, Data Encipherment
+            X509v3 Extended Key Usage: 
+                TLS Web Server Authentication
+    Signature Algorithm: sha256WithRSAEncryption
+         bf:9d:6e:2f:cc:40:9b:92:29:c2:f1:0a:85:6c:35:eb:8e:fa:
+         13:0c:53:58:33:5f:7b:09:58:5f:dd:94:7e:2c:65:ed:73:91:
+         2a:6b:cc:2d:ec:26:1c:8e:95:57:d9:35:19:82:4f:42:59:81:
+         d9:b7:bb:08:70:28:70:35:50:f6:6a:46:e0:2a:ab:90:50:5a:
+         dc:b0:c3:b8:52:d7:5c:90:8f:4c:61:09:2c:ba:4a:31:37:6f:
+         e0:b9:6b:98:dd:aa:dd:52:66:7e:06:f1:8a:4b:bc:23:0d:62:
+         d3:b9:86:8f:3e:cc:05:2b:4d:c4:ad:cf:ae:be:33:22:f6:95:
+         00:f0:36:96:26:5e:42:84:d0:2a:79:41:1e:18:10:1c:96:3e:
+         9a:8b:cc:a5:f9:59:5b:78:d0:a1:a5:2e:4d:55:30:10:0b:cd:
+         13:bc:75:9a:49:e0:de:a4:4d:ed:9b:e8:42:2f:74:2b:dc:6f:
+         2d:d3:38:a9:e8:f8:98:2c:56:aa:3e:dd:0d:48:78:16:4c:50:
+         fd:0a:b3:3c:28:ac:64:7e:e9:bb:10:0e:3b:29:68:40:a9:19:
+         5a:2c:5c:d6:7e:32:39:96:49:a7:4c:6a:a6:09:8e:d4:b8:1e:
+         3e:2c:93:c3:2c:da:f2:09:20:ef:f4:a9:d2:ff:de:cd:7b:20:
+         66:46:ff:c2:36:c3:7d:32:d6:55:d1:fe:0f:00:9a:23:56:97:
+         52:a1:0a:52:64:29:50:c7:5d:b4:1e:e4:67:9a:07:3f:fb:85:
+         03:00:22:d8:f5:e6:bc:95:bf:bc:08:ab:4d:32:4c:d6:52:e0:
+         72:3e:8a:a5:85:72:43:d6:d4:51:6e:99:9a:1f:d8:0e:fd:4d:
+         59:81:7e:c1:81:6d:3b:69:76:ce:53:a4:c0:69:46:72:b2:fe:
+         40:b3:a5:5c:b0:ce:d2:61:83:be:0f:c3:85:a0:21:a7:e8:fd:
+         2f:2c:1c:68:24:1d:9b:a3:43:cb:5e:30:21:af:e8:2e:4e:ec:
+         ea:a7:d2:68:f1:bd:3f:3c:41:48:ac:91:f9:9d:e8:f2:3d:cb:
+         d0:82:d2:00:ed:7b:fa:d8:98:e3:a8:74:f2:ce:70:95:0a:9d:
+         c2:b2:cc:08:d1:fd:de:26:d3:3e:c0:62:28:9b:b4:2d:f4:b5:
+         6d:48:c9:d3:05:f5:1e:68:17:6b:fb:02:2e:20:98:1a:de:d4:
+         ae:6b:e0:68:97:98:e0:4f:47:ec:14:fd:dc:57:d2:e2:5c:59:
+         36:a5:0b:94:b7:4e:b8:ae:ee:c9:ac:02:ae:43:bf:9f:07:da:
+         0c:44:b0:47:69:1d:64:ea:bd:68:af:4f:a7:9a:1f:b1:b9:1d:
+         71:0e:86:4e:0c:ff:a3:1d
+```
+
+#### Let's sign this certificate with our own CA
+
+The CSR will be signed with our previously created ca.key
+We'll sign it to be valid for 10years (3650)
+
+```bash
+$ openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.pem -sha256 -extfile v3.ext
+Signature ok
+subject=C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+Getting CA Private Key
+```
+
+Let's verify what are in this certificate :
+
+```bash
+$ openssl x509 -in server.pem -text -noout
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number:
+            d5:c5:2a:c2:c8:f6:43:c7
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = SonarSource SA
+        Validity
+            Not Before: Mar 17 14:12:29 2020 GMT
+            Not After : Mar 15 14:12:29 2030 GMT
+        Subject: C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (4096 bit)
+                Modulus:
+                    00:a2:43:1e:8b:60:b5:e0:61:3e:99:a4:54:93:c8:
+                    16:14:c2:fa:fd:e5:7c:05:02:71:09:46:d9:2a:52:
+                    57:12:d7:74:46:6a:bd:d4:de:4a:06:b2:51:83:2c:
+                    98:07:8c:b0:f7:e1:8a:aa:fc:0c:30:c6:d7:ec:57:
+                    0b:a7:12:45:e3:13:1a:26:e8:22:d8:fd:2a:9e:ae:
+                    7b:20:b8:41:99:50:0e:b7:1c:bb:78:18:60:25:67:
+                    78:5b:af:d8:7f:d1:01:12:81:0a:1f:dd:f0:54:bc:
+                    57:16:05:22:7c:65:a2:7e:03:ed:e8:7f:50:b1:cd:
+                    7c:e8:7b:58:cb:df:6d:e3:04:03:78:a4:83:e7:20:
+                    c4:37:bc:00:ba:7c:12:d9:ac:52:88:88:72:df:fc:
+                    35:8f:94:f0:1b:33:f8:94:b8:bc:ab:0e:89:68:5f:
+                    92:1b:af:c9:da:c2:c2:e2:a1:c3:8e:c8:16:1a:9e:
+                    89:7a:b4:24:2c:24:df:c5:26:59:ab:d8:f9:06:39:
+                    02:c0:0d:88:5a:0c:14:e7:bc:c5:b8:4c:e5:e0:85:
+                    b2:0b:88:36:b3:d5:35:10:e9:b8:5a:48:69:1a:b3:
+                    2a:4a:d6:f3:f5:6a:91:41:f8:1e:da:d0:0e:21:c3:
+                    a2:f8:5c:08:42:a2:2b:13:be:63:e5:67:d5:19:2f:
+                    2c:96:6d:17:1c:7f:34:19:68:cf:91:b6:14:d9:9a:
+                    1b:1c:f9:08:d7:f9:2d:c3:48:14:3d:02:d4:90:f7:
+                    f2:74:65:f8:22:2d:46:b2:76:cd:46:c1:8e:ab:a1:
+                    11:d7:12:14:77:e3:1c:c3:1c:fa:32:79:0e:0e:59:
+                    55:e4:9d:60:d7:18:0b:25:82:97:28:30:df:de:89:
+                    5b:56:37:a2:33:86:26:12:83:75:f0:02:ae:88:b5:
+                    d6:5e:a2:b7:e7:57:9d:de:72:ad:d6:55:2a:e1:a8:
+                    4c:15:18:a9:e3:22:52:f1:74:e1:b0:d2:e7:9b:ec:
+                    f9:6d:5f:86:c2:9c:e2:22:f2:f4:11:a2:d1:71:b8:
+                    77:e4:8c:4c:ed:84:e8:f9:82:a2:f1:73:95:19:08:
+                    92:d5:b3:50:be:bc:c2:ec:0e:d7:da:53:d2:22:36:
+                    c8:d8:48:d1:22:0d:42:a7:68:6d:e5:b6:5f:00:7d:
+                    70:e4:5f:fe:df:db:3a:96:30:c8:76:89:e9:d1:98:
+                    1e:63:e2:d0:29:46:b0:3d:f6:38:d7:07:40:47:0e:
+                    a3:a5:70:1c:8b:80:c1:81:d1:35:cd:3d:93:20:c6:
+                    7c:10:a4:09:ed:41:12:2e:c3:66:e5:47:96:58:de:
+                    53:1b:d5:67:2c:1d:55:3b:c1:03:28:cf:5e:aa:33:
+                    2b:8c:e1
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Authority Key Identifier: 
+                keyid:26:4F:F6:F9:E6:8B:B6:F7:59:CE:30:23:5C:90:2E:AE:7A:20:C4:DB
+
+            X509v3 Basic Constraints: 
+                CA:FALSE
+            X509v3 Key Usage: 
+                Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
+            X509v3 Subject Alternative Name: 
+                DNS:localhost
+    Signature Algorithm: sha256WithRSAEncryption
+         b0:df:99:da:44:e1:22:c6:51:da:e1:b5:a9:fd:fe:82:d6:74:
+         07:ad:d4:b4:f8:29:3e:57:7a:1b:98:36:4e:0a:23:68:f5:27:
+         c7:52:59:90:cd:94:23:08:83:6f:a4:af:14:a3:e3:ed:f2:13:
+         e4:17:f7:7c:27:45:bc:8c:9a:1d:f3:90:c6:b4:3e:e8:7a:c1:
+         18:e4:8e:8c:28:ac:02:c7:d1:4c:e3:67:7a:13:69:ff:a4:74:
+         c4:82:d7:54:d3:cb:7b:4e:f9:25:36:90:33:43:f0:b8:a5:e6:
+         7c:ea:3d:41:fe:51:3c:bc:d2:c6:4e:9c:dc:04:69:23:08:70:
+         bf:69:2a:bd:28:8c:3f:a1:f0:b0:88:87:a2:af:63:85:86:e3:
+         07:2a:74:89:d0:69:b3:8c:7d:a5:db:ec:f2:5c:56:33:89:04:
+         c6:75:a9:a2:b8:c0:1b:b5:dd:0f:96:50:71:ad:39:36:39:13:
+         d0:80:f3:c8:50:db:d2:65:4d:56:75:9c:70:c2:d6:0c:6b:4a:
+         6e:f7:f1:76:1b:82:16:13:eb:37:4f:05:fd:8f:06:89:15:d7:
+         6d:a7:4e:43:bb:ee:b1:a8:c0:f4:cd:d7:1f:17:c3:3f:1a:79:
+         8f:6e:46:a4:e5:1f:82:8d:60:6f:6c:a2:f4:9b:6e:59:85:48:
+         73:ae:78:dd:c1:fa:81:1f:38:56:84:fc:31:98:af:a8:e4:bf:
+         62:45:16:38:4a:5d:0e:6a:c4:bf:e1:9b:2b:c4:eb:dc:d4:85:
+         82:0f:6c:31:54:1c:46:62:51:22:c3:0d:e4:ca:2e:c9:5f:f5:
+         8c:7a:8c:c2:1d:f2:a8:f9:65:e6:ca:4e:6d:21:4e:55:07:6c:
+         58:0d:fd:59:76:9c:65:7f:26:8f:8b:7b:01:70:5f:59:25:66:
+         a8:9b:0a:70:a1:d8:fd:61:26:7e:4d:5f:3c:28:74:2b:94:fb:
+         2a:8e:35:51:77:5a:96:a9:9b:4e:18:b6:6d:0b:55:4e:2e:15:
+         ca:e7:cb:15:29:0e:b9:fd:23:56:a7:ad:dc:a1:b9:1b:1b:19:
+         24:10:e3:a5:cb:69:2b:40:74:3c:3e:31:ac:a9:0d:17:6b:51:
+         61:d4:5e:d1:98:b6:81:29:55:92:1f:00:8d:4c:72:d4:3a:0e:
+         fd:1f:30:73:04:b8:99:6f:27:57:9a:6c:2b:e1:fa:c2:d3:bf:
+         d3:d2:24:f3:5c:30:a3:25:d6:f5:18:91:13:d4:55:1e:33:89:
+         b7:99:27:a9:14:e4:d9:32:50:ba:56:2f:53:b7:a1:d7:d3:14:
+         2f:e2:73:5a:d4:b2:94:73:14:ef:ac:6f:a1:c1:84:31:17:fd:
+         fa:f8:62:d3:eb:a5:8a:34
+```
+
+#### Let's create a PKCS12 file to be used for starting a TLS server
+
+```bash
+$ openssl pkcs12 -export -in server.pem -inkey server.key -name localhost -out server.p12
+Enter Export Password: pwdServerP12
+Verifying - Enter Export Password: pwdServerP12
+```
+
+The password of the PKCS12 file is `pwdServerP12`
+
+The `server.p12` file can now be used to start a TLS server.
+
+#### Now we'll generate the `client-truststore.p12` file that will have the server CA certificate. 
+Since we don't need to add the key of the certificate (only required to sign, not to verify), we can import it directly with keytool.
+
+```bash
+$ keytool -import -trustcacerts -alias server-ca -keystore client-truststore.p12 -file ca.crt
+Enter keystore password: pwdClientWithServerCA 
+Re-enter new password: pwdClientWithServerCA
+Owner: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Issuer: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Serial number: ed8bcadd4888ffac
+Valid from: Sat Sep 15 08:10:22 CEST 2018 until: Tue Sep 12 08:10:22 CEST 2028
+Certificate fingerprints:
+        MD5:  25:38:06:14:D0:B3:36:81:65:FC:44:CA:E3:BA:57:12
+        SHA1: 77:56:EF:C7:2F:5A:29:D1:A0:54:5F:F8:B4:19:60:91:7B:71:E4:2C
+        SHA256: 1D:2D:E5:52:21:60:75:08:F3:0A:B3:93:CF:38:F6:30:88:56:28:73:20:BA:76:9A:C0:A1:D7:8C:4D:D3:84:AA
+Signature algorithm name: SHA256withRSA
+Subject Public Key Algorithm: 4096-bit RSA key
+Version: 3
+
+Extensions: 
+
+#1: ObjectId: 2.5.29.35 Criticality=false
+AuthorityKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68   BD D6 44 99 0E AD FC FC  ...#...h..D.....
+0010: A5 31 90 D4                                        .1..
+]
+]
+
+#2: ObjectId: 2.5.29.19 Criticality=true
+BasicConstraints:[
+  CA:true
+  PathLen:2147483647
+]
+
+#3: ObjectId: 2.5.29.37 Criticality=false
+ExtendedKeyUsages [
+  serverAuth
+]
+
+#4: ObjectId: 2.5.29.15 Criticality=false
+KeyUsage [
+  DigitalSignature
+  Key_Encipherment
+  Data_Encipherment
+  Key_CertSign
+  Crl_Sign
+]
+
+#5: ObjectId: 2.5.29.14 Criticality=false
+SubjectKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68   BD D6 44 99 0E AD FC FC  ...#...h..D.....
+0010: A5 31 90 D4                                        .1..
+]
+]
+
+Trust this certificate? [no]:  yes
+Certificate was added to keystore
+```
+
+### Create a certificate that will be used to authenticate a user
+
+The principle is the same we'll have a CA authority signing certificates that will be sent by the user to the server.
+In this case the server will have to host the CA authority in its TrustedKeyStore while the client will host his certificate in is KeyStore.
+In this use case, the extensions are not the same, so we'll use openssl-client-auth.conf
+
+#### Generation of CA
+
+One line to generate both the key `ca-lient-auth.key` and the CA certificate `ca-client-auth.crt`
+
+```bash
+openssl req -newkey rsa:4096 -nodes -keyout ca-client-auth.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca-client-auth.crt -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=SonarSource/' -config ./openssl-client-auth.conf
+Generating a 4096 bit RSA private key
+...................................++
+............................................................................................................................................................................................................................................................++
+writing new private key to 'ca-client-auth.key'
+-----
+
+```
+
+For the certificate, the Common Name is used to identify the user
+```bash
+$ openssl req -new -keyout client.key -out client.csr -nodes -newkey rsa:4096 -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=Julien Henry/' -config ./openssl-client-auth.conf
+Generating a 4096 bit RSA private key
+..............................................++
+................++
+writing new private key to 'client.key'
+-----
+```
+
+Let's sign this certificate
+```bash
+$ openssl x509 -req -days 3650 -in client.csr -CA ca-client-auth.crt -CAkey ca-client-auth.key -CAcreateserial -out client.pem -sha256
+Signature ok
+subject=C = CH, ST = Geneva, L = Geneva, O = SonarSource SA, CN = Julien Henry
+Getting CA Private Key
+```
+
+Let's create the pkcs12 store containing the client certificate
+
+```bash
+$ openssl pkcs12 -export -in client.pem -inkey client.key -name julienhenry -out client.p12
+Enter Export Password: pwdClientCertP12
+Verifying - Enter Export Password: pwdClientCertP12
+```
+
+This will go to client keyStore.
+Now we'll generate the `server-with-client-ca.p12` file that will have the CA certificate. Since we don't need to add the key of the certificate (only required to sign, not to verify), we can import it directly with keytool.
+
+```bash
+$ keytool -import -trustcacerts -alias client-ca -keystore server-with-client-ca.p12 -file ca-client-auth.crt
+Enter keystore password: pwdServerWithClientCA 
+Re-enter new password: pwdServerWithClientCA
+Owner: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Issuer: CN=SonarSource, O=SonarSource SA, L=Geneva, ST=Geneva, C=CH
+Serial number: ed8bcadd4888ffac
+Valid from: Sat Sep 15 08:10:22 CEST 2018 until: Tue Sep 12 08:10:22 CEST 2028
+Certificate fingerprints:
+        MD5:  25:38:06:14:D0:B3:36:81:65:FC:44:CA:E3:BA:57:12
+        SHA1: 77:56:EF:C7:2F:5A:29:D1:A0:54:5F:F8:B4:19:60:91:7B:71:E4:2C
+        SHA256: 1D:2D:E5:52:21:60:75:08:F3:0A:B3:93:CF:38:F6:30:88:56:28:73:20:BA:76:9A:C0:A1:D7:8C:4D:D3:84:AA
+Signature algorithm name: SHA256withRSA
+Subject Public Key Algorithm: 4096-bit RSA key
+Version: 3
+
+Extensions: 
+
+#1: ObjectId: 2.5.29.35 Criticality=false
+AuthorityKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68   BD D6 44 99 0E AD FC FC  ...#...h..D.....
+0010: A5 31 90 D4                                        .1..
+]
+]
+
+#2: ObjectId: 2.5.29.19 Criticality=true
+BasicConstraints:[
+  CA:true
+  PathLen:2147483647
+]
+
+#3: ObjectId: 2.5.29.37 Criticality=false
+ExtendedKeyUsages [
+  serverAuth
+]
+
+#4: ObjectId: 2.5.29.15 Criticality=false
+KeyUsage [
+  DigitalSignature
+  Key_Encipherment
+  Data_Encipherment
+  Key_CertSign
+  Crl_Sign
+]
+
+#5: ObjectId: 2.5.29.14 Criticality=false
+SubjectKeyIdentifier [
+KeyIdentifier [
+0000: 87 B9 C1 23 E2 F1 A3 68   BD D6 44 99 0E AD FC FC  ...#...h..D.....
+0010: A5 31 90 D4                                        .1..
+]
+]
+
+Trust this certificate? [no]:  yes
+Certificate was added to keystore
+
+```
\ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.crt
new file mode 100644 (file)
index 0000000..9b362eb
--- /dev/null
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFvzCCA6egAwIBAgIUXBeKw1w29u0HMpY3ywBefre2RewwDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu
+ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTEUMBIGA1UEAwwLU29uYXJTb3Vy
+Y2UwHhcNMjQwNDE2MDkzNTM3WhcNMzQwNDE0MDkzNTM3WjBeMQswCQYDVQQGEwJD
+SDEPMA0GA1UECAwGR2VuZXZhMQ8wDQYDVQQHDAZHZW5ldmExFzAVBgNVBAoMDlNv
+bmFyU291cmNlIFNBMRQwEgYDVQQDDAtTb25hclNvdXJjZTCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBAM1czueBEjAXOWZcZiX6wpqMNqExQHnoMnG+wvzc
+hCHHEkVQIUh/TSmvopfOuuSnr3EXvydXBZS4phmsobNKkZ02mSQNObiB5e0cxntI
+y5CxaMerhT/JJuGxh/+Imwru/F/x/ErCiHqn92fw4yTstGqSP73fjdCBJcuLyRGa
+nYzCykCN5GCvSQVglS62RaNSmBDcL4XEGYdO6PRZVbJ5k0luzK4knPc0s5Sd0eo5
+VzPBvkRHpAcRSUfunGn75rgC5Izy84nDA4KbOk2d9tQMpMHWs9Y5Z4qZJsII3dXz
+ZH4k8jJzt3VWvHbG/1+CmUdEW/62AK3aVLtmuygsD1NHMALIaF96bf22Ga646OFK
+Qw9FGHr4d3+LbG4ZvdNhPByPtZC5YU4XMk7agnH6k0O2szwEP53eGp+YWSKcZ3o9
+m2XnTfOyUzfF8luDww6qrpmNkuSfQVGhWevSQRNSjz5esFKhdW3Upnx5WY0jf9KQ
+n9Tsw6yJ6GG/ZzRL5uMKunyfrEeCXYZWAlQ1G2DSLQw0+L1ysbWn49NNw8P1l3xi
+RbegcR4QSreBsf65u7ZRV1iaECRD2uvylqxK/5Y6me+0bfAZ1set974N8pyDiuXc
+LEeKnfo5TcHhXEfJEOfSGrGZKgV+54FOzoOcy088gPkRIUykbghJC4vIiKzgwp/i
+1QlZAgMBAAGjdTBzMAsGA1UdDwQEAwIBtjATBgNVHSUEDDAKBggrBgEFBQcDATAd
+BgNVHQ4EFgQUbAPAj0KGCnTshl0HEOdGteNZvTEwHwYDVR0jBBgwFoAUbAPAj0KG
+CnTshl0HEOdGteNZvTEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AgEAGIKrSLleiv8rUcysRdB0hkiLLNFCDR1/DgZd+ZHmqGkeZpAdS4Q+Y1PgKewQ
+nave2KbCTjoRsh8EBU0/ZVKDXKs2IN63c0B0Bc5GYqYq27vBiVrkkPGawWjHaI8M
+AdESBtGBgd4F2SP9PQNFuLSj986NTu4WiRULwHKHidBwQVE6nDWif4TSWrELDbG8
+VcP1uWNLT68hMGBrbNL1vyScZImUqNyRMMtbQb9IBF7e0wgcIm5xewu/MQwYQAMG
+ap8lalHjfTrb3DtOV/pjQEI6c8xTvPF6w0FfPUE6ViXipn4Z7yFbPbIrRlcAmcG9
+YW4fflIza+x06IvqTHZVYH+G3xyA6V9Qq/dSniwp8mJU+g3aeMMWa5M1xgEO+4B1
+Xv4Fj3Z3YfU50ZBycoXVxAvDOXSoGnR+Q4Wgk5G/qvXX3ZBd3U3mieOkJ8L31pHk
+i2KPuYtm8ynrRc4InqAxKCJkFG8ylqnNCQ+Hl9qfa+PQzeGxwK5/wjztF/rjOYep
+eed+0DbpV7dwxVhIhBbdu++rjGejzB6qRORwYoGoHYLX6LxdK4LU8g3eLxA/SKs8
+8jeXpNYI8ucX6EkSZAPkvNfgQw5Akbd8oNf8rJvpGdV3vb4+yraZH400Do5+SUeu
+VHlCQX+vlBXfZbQAgRuFaXj2AWu2Ipp6DLYA1Hx89b3190Y=
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key b/sonar-scanner-engine/src/test/resources/ssl/ca-client-auth.key
new file mode 100644 (file)
index 0000000..7ac7d7d
--- /dev/null
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+53j8dPRgcqZGjgO/EG0uAj3DINgNOg==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca.crt b/sonar-scanner-engine/src/test/resources/ssl/ca.crt
new file mode 100644 (file)
index 0000000..8259e83
--- /dev/null
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/ca.key b/sonar-scanner-engine/src/test/resources/ssl/ca.key
new file mode 100644 (file)
index 0000000..cb3a442
--- /dev/null
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
++iNXE0r4lx4TmqctoBApwtiahBnPfg==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12 b/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12
new file mode 100644 (file)
index 0000000..21d251e
Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/ssl/client-truststore.p12 differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.csr b/sonar-scanner-engine/src/test/resources/ssl/client.csr
new file mode 100644 (file)
index 0000000..1dea36d
--- /dev/null
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIE9TCCAt0CAQAwXzELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G
+A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTEVMBMGA1UEAwwM
+SnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1d3G
+hgrgkmtWl2rMZBMWTUIONuc7nDRG+DmvsOesTTx9AjCO0Wn3q+4Zj3LLQiq9j/N0
+goI4BrBluq4Nz6d08y0O5zOPJg9qCQpPlEWXSbrK39OMjRHJn+WmKnHF59EAo0Se
+8upR5yF1xBfmb/DGBm30eWnZDf9XKXDqZV+GgT/d/3+569kuJGJEcjuj+vrNM3z/
+WXSUS7mNAaDqUApqAZ4/kQa0fybGpC2MwfEzESS+R2RHyPQy1HVXsqkrTnxPSWZO
+DC20uWXDk1IlCCgqzKgkbjTvFMsn9GWwaYjcZQXe2b/pgWWMRc31hp6sjUO9EYOn
+Y7KLbVsJ558LlzK+8t45kpvPp2OHTPjs+ledSr+m0LAVFn6uXMOCudpo41yHggol
+Ep4OpxytkvDfozs9teHbh0NooWnwo65+h7YsEB9CGzG9tIeWZdu51KlPPAYpNafT
+9gUR4mDT4a20vGzHb3KtYmPQp6MQc/WwBajW7lE0jFTkukCNhBgL8csEZLgusYui
+36mnct3VMPaQXgGQJaR5//Zmp09aTwB9ILl7bzHAG3i5Um3OdNtTa3VW/N1U9lrT
+sNrRz2IfEWAwWFhK/oTcyhBx0kIfW5ju2cWbD76Yfq27uiHgYKTPJI/kN/8Vursx
+ex7rFKiU+hIvFbB61mqBN9OG7TKjWnJF8oa9GjkCAwEAAaBRME8GCSqGSIb3DQEJ
+DjFCMEAwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwEwYDVR0lBAwwCgYIKwYBBQUH
+AwIwEQYJYIZIAYb4QgEBBAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQATY8bk0rcI
+lwMw9FAYOTrQy4ZVuSEymby01ebjJ6I/1s+UQa6TI9pDigvomy+CgUWdXhquzQ8B
+dnOJGibTc3H2iMD5sb0+QhqhJtmCASup9ar1+JQTBEfcorVo2/8qkoEg5oDP+VEI
+oHiVzK5LRxfXwHiAoZURi/VJjp+0JJYaCBKrb8hVoGVStTG6qNKFi685v6KhVmM8
+Ng3JzuwC15e6LQ9Nr0xULaGcOZR0N/wkUl+2ntXDLIiVORyXmv4lY/Jp7bHhH7qr
+SecpEROzOZzIdzG4Iyy5Mj/DIPqKk55f5HuVXyhRiar89TBKrIwBVytlKPOZH4Ot
+rUTbKnz6OqBvXPFV6+YNyQb4LesU0CSWUVzFtYygBk1fcr+l7Bluxk29FTDf9iZL
+sc5v6fvrBhJmKHBQVTR1h9MvRpgpuImvRVdmOKNDXY7KpC7kB+DjKFEoQEtqJG0D
+53dxbkH7oaz57oSXPYjsAlezsDbAlWRFPD4gA+TqAJ0BkzoTggmctgJ8EpVkgzvG
+7iQAV+KN7OMClft4AQQO4224fSi9t8PSy+pWBc5wVDWLmnWxIkBHyBs+JgYJZ9pX
++Xo+S+FDxIK6ALirSeJlTDvIKgjgtLzXA6a+BNMNyQPKZHIjRqU36L/7fYyZnmTg
+aoBsmYBIm3xEMVi0VDBNFRPmLugPM2v9GQ==
+-----END CERTIFICATE REQUEST-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.key b/sonar-scanner-engine/src/test/resources/ssl/client.key
new file mode 100644 (file)
index 0000000..42d414d
--- /dev/null
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+vsseryZb70+/NjZqWcSTcuFhooe9+Q==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.p12 b/sonar-scanner-engine/src/test/resources/ssl/client.p12
new file mode 100644 (file)
index 0000000..d0a7a1f
Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/ssl/client.p12 differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/client.pem b/sonar-scanner-engine/src/test/resources/ssl/client.pem
new file mode 100644 (file)
index 0000000..1b5e27a
--- /dev/null
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFRDCCAywCFF+7ThfA7f+eATq97zVNq8PRccLPMA0GCSqGSIb3DQEBCwUAMF4x
+CzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExDzANBgNVBAcMBkdlbmV2YTEX
+MBUGA1UECgwOU29uYXJTb3VyY2UgU0ExFDASBgNVBAMMC1NvbmFyU291cmNlMB4X
+DTI0MDQxNjA5MzYwM1oXDTM0MDQxNDA5MzYwM1owXzELMAkGA1UEBhMCQ0gxDzAN
+BgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNv
+dXJjZSBTQTEVMBMGA1UEAwwMSnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA1d3GhgrgkmtWl2rMZBMWTUIONuc7nDRG+DmvsOesTTx9
+AjCO0Wn3q+4Zj3LLQiq9j/N0goI4BrBluq4Nz6d08y0O5zOPJg9qCQpPlEWXSbrK
+39OMjRHJn+WmKnHF59EAo0Se8upR5yF1xBfmb/DGBm30eWnZDf9XKXDqZV+GgT/d
+/3+569kuJGJEcjuj+vrNM3z/WXSUS7mNAaDqUApqAZ4/kQa0fybGpC2MwfEzESS+
+R2RHyPQy1HVXsqkrTnxPSWZODC20uWXDk1IlCCgqzKgkbjTvFMsn9GWwaYjcZQXe
+2b/pgWWMRc31hp6sjUO9EYOnY7KLbVsJ558LlzK+8t45kpvPp2OHTPjs+ledSr+m
+0LAVFn6uXMOCudpo41yHggolEp4OpxytkvDfozs9teHbh0NooWnwo65+h7YsEB9C
+GzG9tIeWZdu51KlPPAYpNafT9gUR4mDT4a20vGzHb3KtYmPQp6MQc/WwBajW7lE0
+jFTkukCNhBgL8csEZLgusYui36mnct3VMPaQXgGQJaR5//Zmp09aTwB9ILl7bzHA
+G3i5Um3OdNtTa3VW/N1U9lrTsNrRz2IfEWAwWFhK/oTcyhBx0kIfW5ju2cWbD76Y
+fq27uiHgYKTPJI/kN/8Vursxex7rFKiU+hIvFbB61mqBN9OG7TKjWnJF8oa9GjkC
+AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAfmCoELV+5Nk4Cz0FP6xT2FP1YNXJJR2u
+qNilLDL6TqD/ONgPdmbMgwx5dNcSxOL8Fg2dUENgjwyl96WAfnFy+F6V9PFZmVk3
+BE0CWBMMb37Vr3gptyBdV9sOtgM4lIUwu+PYh4ykvk+cd6l7GfWBIDKpKA0mXJyY
+V3IkaFuxboOYE8CptNMH42vYPfAxQi+hFLzj97+Jhb/Ycv7gZHEvjt+EtSMmPTta
+OtOVMV0c8rFgvFraFloX0idEswBRzt+LG1F10zXQwG3aRLqde9t/SiQuTL1pctLt
+VMidqUSHfVYJOiqKnwZ+SWq75VRPBRSPowriZG2bOuLpkAYCEOU5+tYh3+pcGptq
+dl8Zcxrhmc9/LcDjPNtvxSNKQoM5rCVW2BRdqv0E0uxujlxtiWfQ3UnjfLdJgW1Y
+wF5kMSn6A7Nt+SkLMfao4Y1Toj/EywPvpnf8Dw4DwYd7UtKXKdY7i1mBAmqmSemN
+M2VLsP+O8+njHP+qtXPBsU8PBrgGpDh4zgmEGUoAPDea9uhX7F0ArSxZhOUpDGIj
+8HNBN52R+lZpjIZeIQlS2sIUGVm7XQY8I/11u26VgiKyPJxUOp19RjLzulk0v7yb
+MOGdW8OcQ03On9ucoTC2AbknqxX5/3fmH3f5ztbcGtZ0lE5LihySNyk7wLgkqHUA
+8JnfYM95xuk=
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf b/sonar-scanner-engine/src/test/resources/ssl/openssl-client-auth.conf
new file mode 100644 (file)
index 0000000..a80df82
--- /dev/null
@@ -0,0 +1,33 @@
+HOME            = .
+RANDFILE        = $ENV::HOME/.rnd
+
+[ req ]
+default_bits       = 4096
+distinguished_name = req_distinguished_name
+req_extensions     = client_extensions
+
+[ req_distinguished_name ]
+countryName                   = Country Name (2-letter code)
+countryName_default           = CH
+stateOrProvinceName           = State or Province Name (full name)
+stateOrProvinceName_default   = Geneva
+localityName                  = Locality (e.g. city name)
+localityName_default          = Geneva
+organizationName              = Organization (e.g. company name)
+organizationName_default      = SonarSource SA
+commonName                    = Common Name (your.domain.com)
+commonName_default            = Julien Henry
+
+[ client_extensions ]
+basicConstraints = CA:FALSE
+keyUsage = digitalSignature, keyEncipherment, dataEncipherment
+extendedKeyUsage = clientAuth
+nsCertType = client
+
+[ ca_extensions ]
+basicConstraints = CA:FALSE
+keyUsage = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature
+extendedKeyUsage = serverAuth
+subjectKeyIdentifier   = hash
+authorityKeyIdentifier = keyid:always, issuer
+basicConstraints       = critical, CA:true
\ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/openssl.conf b/sonar-scanner-engine/src/test/resources/ssl/openssl.conf
new file mode 100644 (file)
index 0000000..dc674db
--- /dev/null
@@ -0,0 +1,34 @@
+HOME            = .
+
+[ req ]
+default_bits       = 4096
+distinguished_name = req_distinguished_name
+req_extensions     = req_extensions
+
+[ req_distinguished_name ]
+countryName                   = Country Name (2-letter code)
+countryName_default           = CH
+stateOrProvinceName           = State or Province Name (full name)
+stateOrProvinceName_default   = Geneva
+localityName                  = Locality (e.g. city name)
+localityName_default          = Geneva
+organizationName              = Organization (e.g. company name)
+organizationName_default      = SonarSource SA
+commonName                    = Common Name (your.domain.com)
+commonName_default            = localhost
+
+[ req_extensions ]
+subjectAltName    = @alt_names
+keyUsage          = keyEncipherment, dataEncipherment, digitalSignature
+extendedKeyUsage  = serverAuth
+
+[ ca_extensions ]
+basicConstraints        = CA:FALSE
+keyUsage                = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature
+extendedKeyUsage        = serverAuth
+subjectKeyIdentifier    = hash
+authorityKeyIdentifier  = keyid:always, issuer
+basicConstraints        = critical, CA:true
+
+[ alt_names ]
+DNS.1 = localhost
\ No newline at end of file
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12 b/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12
new file mode 100644 (file)
index 0000000..d95e0a9
Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/ssl/server-with-client-ca.p12 differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.csr b/sonar-scanner-engine/src/test/resources/ssl/server.csr
new file mode 100644 (file)
index 0000000..a5028f0
--- /dev/null
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIE6jCCAtICAQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G
+A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJ
+bG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRRQF25E
+5NCgXdoEBU2SWyAoyOWMGVT1Ioltnr3sJP6LMjjfozK5YgaRn504291lwlG+k6tv
+zTSR9HB8q3ITa8AdnwMiL7jzbveYKWIlLQ7kdHKXWbiaIjTaZCyfnWUlDFIuR7BH
+wOXVwyLrBQfhoyDVaaoyowQEsUro3okIR/kBsqM+KH8bcdl06DMMppZ8Qy1DYvPo
+dhnNRyOSSpfbIoodE1fju+5U0OKzvGIc9WpG5pKIysaW3whOa/ieb02SXrgoiHnY
+PpmmGzm4u/Wn8jGwhYQJSQT10yjMacGHwmBEq7FUr854cVd+eend056P6pwUukdN
+eVHCFjYRkmWCNzIxV+sS9PPtDs77/bLFIItrnBMHVsId38tPoru/z1S1p2dzCX3N
+q09aJFF/vH2u9Sg5aerHJ7xnRroR1jIrAZtcjBkJHEiTlG+WaavP4j6oym+lvHvg
+HHL3Qwhh8emg0JiLYExVV7ma70aRDh8yoQtSzAUDMVfhVPKd92MS+7DC2pv2KviU
+NKqbHDFadl01JN3t+17/gstUNSk1jpoUfUhKBeUQxVEdVUy2p0HeD/TYpRvF2FEs
+Wneq3+ZbnRp17I/uEQOck0LP2tkzAd4tmRgH+95yyB8MgbAfvyKWkB4+3BhtdfoY
+De1asqR6z43mejDHHqgBXn+u3UKjPypKfPECAwEAAaBJMEcGCSqGSIb3DQEJDjE6
+MDgwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAsGA1UdDwQEAwIEsDATBgNVHSUEDDAK
+BggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAgEAWLCYfUyCFT1bOafpcrjdpHaE
+Z+DgrFniECq/gbx3DYdYHCxmfXVKuLGby2uNoujPPW4pIRzfbFjijoHN50uF8tnX
+qtpHlEACoFnpMUOfTyP9FnjT33VdSNoMdXv1xfRtIZE+WUE9CemYFBChGobo861a
+au3QYyyHiSTeAsE/cNOBoaNwvUieTRjX8NAq69vkkmFaEIstKingEt3HVMD3yZPu
+qIMRuzrH63H0YOf9BsHieRTbV4h8FzmJyuaHMGpmBbP5zM3ynvoBlCki3sLYQQ78
+jrfxLHpco40Re4/Er9qra5LtEYBZEaooTHd1oPxyIxGiaw6pLLi/VlWy64L9Bt9w
+Ta2suIHoUElL7F+QXkh6+16ljdghjJCvhIrFOdf7hzcG+x3u4H24Nm5bxYXiSpQD
+DAkj1mzuO49iqRhytEh6LMAXtMp8ntX79gYLkyaa0TRChYqwYNNlTzAmsG4vO7Hy
++dOc1hF5RIQd3TL7jmGDLlI+Y4DXrPMVa/6edimIcBFCgdYau70vBK9P5z+5HroL
+AZS2/kxTT3eaWBF77mCfyqXq/qfiUaQk1d7qszfJxpdbar4tJqOPXxJa8mK1AIuI
+tezgiHDB+4mM9XhVF2tdIekLfvVuRIKOA+9xbPtnrYRngH0VLcPOYlt+eC95N45D
+cMyWA9ULzIcwEeBjYSE=
+-----END CERTIFICATE REQUEST-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.key b/sonar-scanner-engine/src/test/resources/ssl/server.key
new file mode 100644 (file)
index 0000000..c0568c3
--- /dev/null
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+9uPzaGE09HfpBsTJtUTjU0mClVlOCw==
+-----END PRIVATE KEY-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.p12 b/sonar-scanner-engine/src/test/resources/ssl/server.p12
new file mode 100644 (file)
index 0000000..17a81a1
Binary files /dev/null and b/sonar-scanner-engine/src/test/resources/ssl/server.p12 differ
diff --git a/sonar-scanner-engine/src/test/resources/ssl/server.pem b/sonar-scanner-engine/src/test/resources/ssl/server.pem
new file mode 100644 (file)
index 0000000..6179220
--- /dev/null
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtjCCA56gAwIBAgIULroxFuPWyNOiQtAVPS/XFFMXp6owDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu
+ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0
+MB4XDTI0MDQxNjA4NDUyMVoXDTM0MDQxNDA4NDUyMVowXDELMAkGA1UEBhMCQ0gx
+DzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25h
+clNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEArRRQF25E5NCgXdoEBU2SWyAoyOWMGVT1Ioltnr3sJP6L
+MjjfozK5YgaRn504291lwlG+k6tvzTSR9HB8q3ITa8AdnwMiL7jzbveYKWIlLQ7k
+dHKXWbiaIjTaZCyfnWUlDFIuR7BHwOXVwyLrBQfhoyDVaaoyowQEsUro3okIR/kB
+sqM+KH8bcdl06DMMppZ8Qy1DYvPodhnNRyOSSpfbIoodE1fju+5U0OKzvGIc9WpG
+5pKIysaW3whOa/ieb02SXrgoiHnYPpmmGzm4u/Wn8jGwhYQJSQT10yjMacGHwmBE
+q7FUr854cVd+eend056P6pwUukdNeVHCFjYRkmWCNzIxV+sS9PPtDs77/bLFIItr
+nBMHVsId38tPoru/z1S1p2dzCX3Nq09aJFF/vH2u9Sg5aerHJ7xnRroR1jIrAZtc
+jBkJHEiTlG+WaavP4j6oym+lvHvgHHL3Qwhh8emg0JiLYExVV7ma70aRDh8yoQtS
+zAUDMVfhVPKd92MS+7DC2pv2KviUNKqbHDFadl01JN3t+17/gstUNSk1jpoUfUhK
+BeUQxVEdVUy2p0HeD/TYpRvF2FEsWneq3+ZbnRp17I/uEQOck0LP2tkzAd4tmRgH
++95yyB8MgbAfvyKWkB4+3BhtdfoYDe1asqR6z43mejDHHqgBXn+u3UKjPypKfPEC
+AwEAAaNwMG4wHwYDVR0jBBgwFoAUINXfg3fn6/RUenW3EobpMoP8wDQwCQYDVR0T
+BAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQW
+BBRX4bsny+8GQcFpM10jtAfFxzNxzzANBgkqhkiG9w0BAQsFAAOCAgEAa+Myw6li
+Fme95cPpINTite/9LXk+TlHHnXiV5Z+Um3NTLSllX3zPuRFiOE71OKFrWQPqH2N/
+85l6h19G9xQsaqkkVFyQENkNzykZpJL/jU4+wgRtwcEDkaRGGURZacz3vfLTc1HX
+tPDNv/JsZ5HE2d7cF5YhN4UahtxS2lvarrSujaOBpFZTT6PbEYX9EnwCdapORHOh
+wKMc3OGGOiGWvRlVaWu/Huq2HvXXcK0pmaYWWKX3u21evthSYOu9U4Rk0z1y7m3/
+CIYaIrvSbkzq2KKXMn7lr26bv2cthAQrPAjb2ILPUoyzKa3wEK3lkhanM6PN9CMH
+y5KRTpqwV45Qr6BAVY1bP67pEkay2T31chIVKds6dkx9b2/bWpW9PWuymsbWX2vO
+Q1MiaPkXKSTgCRwQUR0SNbPHw3X+VhrKKJB+beX8Bh2fcKw3jGGM8oHiA1hpdnbg
+Y5fW7EupF5gabf2jNB1XJ4gowlpB3nTooKFgbcgsvi68MRdBno2TWUhsZ3zCVyaH
+KFdDV0f78Fg7oL79K3kBL/iqr+jsb8sFHKIS4Dyyz2rDJrE0q0xAPes+Bu75R3/5
+M/s2H7KuLqLdDYsCsMeMqOVuIcAyPp2MFWInYPyi0zY4fwKwm8f/Kv8Lzb+moxqI
+Fct6d1S08JAosVnZcP2P7Yz+TbmDRtsqCgk=
+-----END CERTIFICATE-----
diff --git a/sonar-scanner-engine/src/test/resources/ssl/v3.ext b/sonar-scanner-engine/src/test/resources/ssl/v3.ext
new file mode 100644 (file)
index 0000000..8027d8f
--- /dev/null
@@ -0,0 +1,7 @@
+authorityKeyIdentifier=keyid,issuer
+basicConstraints=CA:FALSE
+keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = localhost
\ No newline at end of file