]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19597 Security fix (SSF-420)
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>
Thu, 15 Jun 2023 05:32:29 +0000 (07:32 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 20 Jun 2023 13:10:17 +0000 (13:10 +0000)
sonar-core/build.gradle
sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java
sonar-core/src/main/java/org/sonar/core/util/HttpsTrust.java [deleted file]
sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderIT.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderTest.java [deleted file]
sonar-core/src/test/java/org/sonar/core/util/HttpsTrustTest.java [deleted file]

index ded22c46c6f2aea054ae49593769da6f56da3eb0..2ba9ea4dbfd53eb78a47a5e09f53b5826318c2ec 100644 (file)
@@ -11,6 +11,7 @@ dependencies {
   api 'ch.qos.logback:logback-core'
   api 'com.google.guava:guava'
   api 'com.google.protobuf:protobuf-java'
+  api 'com.squareup.okhttp3:okhttp'
   api 'commons-codec:commons-codec'
   api 'commons-io:commons-io'
   api 'commons-lang:commons-lang'
@@ -22,6 +23,7 @@ dependencies {
   api 'org.sonarsource.update-center:sonar-update-center-common'
   api 'org.springframework:spring-context'
   api project(':sonar-plugin-api-impl')
+  api project(':sonar-ws')
 
   compileOnlyApi 'com.google.code.findbugs:jsr305'
   compileOnlyApi 'com.google.code.gson:gson'
index 85c56de88be3fa3aca2fb84fadd49b6b5c201170..8db9479e61da5e45e896c52f2927004dc32d99e6 100644 (file)
  */
 package org.sonar.core.util;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.Authenticator;
-import java.net.HttpURLConnection;
-import java.net.PasswordAuthentication;
-import java.net.Proxy;
-import java.net.ProxySelector;
 import java.net.URI;
 import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
 import java.util.Optional;
-import java.util.zip.GZIPInputStream;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
-import org.apache.commons.codec.binary.Base64;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
 import org.apache.commons.io.IOUtils;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.platform.Server;
 import org.sonar.api.utils.HttpDownloader;
 import org.sonar.api.utils.SonarException;
-import org.sonar.api.utils.log.Loggers;
+import org.sonarqube.ws.client.OkHttpClientBuilder;
 
 import static org.apache.commons.io.FileUtils.copyInputStreamToFile;
 import static org.sonar.core.util.FileUtils.deleteQuietly;
@@ -59,42 +49,44 @@ import static org.sonar.core.util.FileUtils.deleteQuietly;
  */
 public class DefaultHttpDownloader extends HttpDownloader {
 
-  private final BaseHttpDownloader downloader;
-  private final Integer readTimeout;
-  private final Integer connectTimeout;
+  private final OkHttpClient client;
 
   @Inject
   public DefaultHttpDownloader(Server server, Configuration config) {
-    this(server, config, null);
-  }
-
-  public DefaultHttpDownloader(Server server, Configuration config, @Nullable Integer readTimeout) {
-    this(server, config, null, readTimeout);
+    this(server, config, null, null);
   }
 
   public DefaultHttpDownloader(Server server, Configuration config, @Nullable Integer connectTimeout, @Nullable Integer readTimeout) {
-    this.readTimeout = readTimeout;
-    this.connectTimeout = connectTimeout;
-    downloader = new BaseHttpDownloader(new AuthenticatorFacade(), config, server.getVersion());
-  }
-
-  public DefaultHttpDownloader(Configuration config) {
-    this(config, null);
+    client = buildHttpClient(server, config, connectTimeout, readTimeout);
   }
 
-  public DefaultHttpDownloader(Configuration config, @Nullable Integer readTimeout) {
-    this(config, null, readTimeout);
+  private static OkHttpClient buildHttpClient(Server server, Configuration config, @Nullable Integer connectTimeout,
+    @Nullable Integer readTimeout) {
+    OkHttpClientBuilder clientBuilder = new OkHttpClientBuilder()
+      .setFollowRedirects(true)
+      .setUserAgent(getUserAgent(server, config));
+    if (connectTimeout != null) {
+      clientBuilder
+        .setConnectTimeoutMs(connectTimeout);
+    }
+    if (readTimeout != null) {
+      clientBuilder
+        .setReadTimeoutMs(readTimeout);
+    }
+    return clientBuilder.build();
   }
 
-  public DefaultHttpDownloader(Configuration config, @Nullable Integer connectTimeout, @Nullable Integer readTimeout) {
-    this.readTimeout = readTimeout;
-    this.connectTimeout = connectTimeout;
-    downloader = new BaseHttpDownloader(new AuthenticatorFacade(), config, null);
+  private static String getUserAgent(Server server, Configuration config) {
+    Optional<String> serverId = config.get(CoreProperties.SERVER_ID);
+    if (serverId.isEmpty()) {
+      return String.format("SonarQube %s #", server.getVersion());
+    }
+    return String.format("SonarQube %s # %s", server.getVersion(), serverId.get());
   }
 
   @Override
   protected String description(URI uri) {
-    return String.format("%s (%s)", uri.toString(), getProxySynthesis(uri));
+    return uri.toString();
   }
 
   @Override
@@ -109,8 +101,8 @@ public class DefaultHttpDownloader extends HttpDownloader {
 
   @Override
   protected String readString(URI uri, Charset charset) {
-    try {
-      return IOUtils.toString(downloader.newInputSupplier(uri, this.connectTimeout, this.readTimeout).getInput(), charset);
+    try (Response response = executeCall(uri)) {
+      return IOUtils.toString(response.body().byteStream(), charset);
     } catch (IOException e) {
       throw failToDownload(uri, e);
     }
@@ -123,21 +115,18 @@ public class DefaultHttpDownloader extends HttpDownloader {
 
   @Override
   public byte[] download(URI uri) {
-    try {
-      return ByteStreams.toByteArray(downloader.newInputSupplier(uri, this.connectTimeout, this.readTimeout).getInput());
+    try (Response response = executeCall(uri)) {
+      return ByteStreams.toByteArray(response.body().byteStream());
     } catch (IOException e) {
       throw failToDownload(uri, e);
     }
   }
 
-  public String getProxySynthesis(URI uri) {
-    return BaseHttpDownloader.getProxySynthesis(uri);
-  }
-
   @Override
   public InputStream openStream(URI uri) {
     try {
-      return downloader.newInputSupplier(uri, this.connectTimeout, this.readTimeout).getInput();
+      Response response = executeCall(uri);
+      return response.body().byteStream();
     } catch (IOException e) {
       throw failToDownload(uri, e);
     }
@@ -145,176 +134,21 @@ public class DefaultHttpDownloader extends HttpDownloader {
 
   @Override
   public void download(URI uri, File toFile) {
-    try {
-      copyInputStreamToFile(downloader.newInputSupplier(uri, this.connectTimeout, this.readTimeout).getInput(), toFile);
+    try (Response response = executeCall(uri)) {
+      copyInputStreamToFile(response.body().byteStream(), toFile);
     } catch (IOException e) {
       deleteQuietly(toFile);
       throw failToDownload(uri, e);
     }
   }
 
-  private SonarException failToDownload(URI uri, IOException e) {
-    throw new SonarException(String.format("Fail to download: %s (%s)", uri, getProxySynthesis(uri)), e);
-  }
-
-  /**
-   * Facade to allow unit tests to verify calls to {@link Authenticator#setDefault(Authenticator)}.
-   */
-  static class AuthenticatorFacade {
-    void setDefaultAuthenticator(Authenticator authenticator) {
-      Authenticator.setDefault(authenticator);
-    }
+  private Response executeCall(URI uri) throws IOException {
+    Request request = new Request.Builder().url(uri.toURL()).get().build();
+    return client.newCall(request).execute();
   }
 
-  static class BaseHttpDownloader {
-
-    private static final String GET = "GET";
-    private static final String HTTP_PROXY_USER = "http.proxyUser";
-    private static final String HTTP_PROXY_PASSWORD = "http.proxyPassword";
-
-    private String userAgent;
-
-    BaseHttpDownloader(AuthenticatorFacade system, Configuration config, @Nullable String userAgent) {
-      initProxy(system, config);
-      initUserAgent(userAgent, config);
-    }
-
-    private static void initProxy(AuthenticatorFacade system, Configuration config) {
-      // register credentials
-      Optional<String> login = config.get(HTTP_PROXY_USER);
-      if (login.isPresent()) {
-        system.setDefaultAuthenticator(new ProxyAuthenticator(login.get(), config.get(HTTP_PROXY_PASSWORD).orElse(null)));
-      }
-    }
-
-    private void initUserAgent(@Nullable String sonarVersion, Configuration settings) {
-      Optional<String> serverId = settings.get(CoreProperties.SERVER_ID);
-      userAgent = sonarVersion == null ? "SonarQube" : String.format("SonarQube %s # %s", sonarVersion, serverId.orElse(""));
-      System.setProperty("http.agent", userAgent);
-    }
-
-    private static String getProxySynthesis(URI uri) {
-      return getProxySynthesis(uri, ProxySelector.getDefault());
-    }
-
-    @VisibleForTesting
-    static String getProxySynthesis(URI uri, ProxySelector proxySelector) {
-      List<Proxy> proxies = proxySelector.select(uri);
-      if (proxies.size() == 1 && proxies.get(0).type().equals(Proxy.Type.DIRECT)) {
-        return "no proxy";
-      }
-
-      List<String> descriptions = Lists.newArrayList();
-      for (Proxy proxy : proxies) {
-        if (proxy.type() != Proxy.Type.DIRECT) {
-          descriptions.add(proxy.type() + " proxy: " + proxy.address());
-        }
-      }
-
-      return Joiner.on(", ").join(descriptions);
-    }
-
-    public HttpInputSupplier newInputSupplier(URI uri, @Nullable Integer connectTimeoutMillis, @Nullable Integer readTimeoutMillis) {
-      return newInputSupplier(uri, GET, connectTimeoutMillis, readTimeoutMillis);
-    }
-
-    public HttpInputSupplier newInputSupplier(URI uri, String requestMethod, @Nullable Integer connectTimeoutMillis, @Nullable Integer readTimeoutMillis) {
-      return newInputSupplier(uri, requestMethod, null, null, connectTimeoutMillis, readTimeoutMillis);
-    }
-
-    public HttpInputSupplier newInputSupplier(URI uri, String requestMethod, String login, String password, @Nullable Integer connectTimeoutMillis,
-      @Nullable Integer readTimeoutMillis) {
-      int read = readTimeoutMillis != null ? readTimeoutMillis : DEFAULT_READ_TIMEOUT_IN_MILLISECONDS;
-      int connect = connectTimeoutMillis != null ? connectTimeoutMillis : DEFAULT_CONNECT_TIMEOUT_IN_MILLISECONDS;
-      return new HttpInputSupplier(uri, requestMethod, userAgent, login, password, connect, read);
-    }
-
-    private static class HttpInputSupplier {
-      private final String login;
-      private final String password;
-      private final URI uri;
-      private final String userAgent;
-      private final int connectTimeoutMillis;
-      private final int readTimeoutMillis;
-      private final String requestMethod;
-
-      HttpInputSupplier(URI uri, String requestMethod, String userAgent, String login, String password, int connectTimeoutMillis, int readTimeoutMillis) {
-        this.uri = uri;
-        this.requestMethod = requestMethod;
-        this.userAgent = userAgent;
-        this.login = login;
-        this.password = password;
-        this.readTimeoutMillis = readTimeoutMillis;
-        this.connectTimeoutMillis = connectTimeoutMillis;
-      }
-
-      /**
-       * @throws IOException any I/O error, not limited to the network connection
-       * @throws HttpException if HTTP response code > 400
-       */
-      public InputStream getInput() throws IOException {
-        Loggers.get(getClass()).debug("Download: " + uri + " (" + getProxySynthesis(uri, ProxySelector.getDefault()) + ")");
-        HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
-        connection.setRequestMethod(requestMethod);
-        HttpsTrust.INSTANCE.trust(connection);
-
-        // allow both GZip and Deflate (ZLib) encodings
-        connection.setRequestProperty("Accept-Encoding", "gzip");
-        if (!Strings.isNullOrEmpty(login)) {
-          String encoded = Base64.encodeBase64String((login + ":" + password).getBytes(StandardCharsets.UTF_8));
-          connection.setRequestProperty("Authorization", "Basic " + encoded);
-        }
-        connection.setConnectTimeout(connectTimeoutMillis);
-        connection.setReadTimeout(readTimeoutMillis);
-        connection.setUseCaches(true);
-        connection.setInstanceFollowRedirects(true);
-        connection.setRequestProperty("User-Agent", userAgent);
-
-        // establish connection, get response headers
-        connection.connect();
-
-        // obtain the encoding returned by the server
-        String encoding = connection.getContentEncoding();
-
-        int responseCode = connection.getResponseCode();
-        if (responseCode >= 400) {
-          InputStream errorResponse = null;
-          try {
-            errorResponse = connection.getErrorStream();
-            if (errorResponse != null) {
-              String errorResponseContent = IOUtils.toString(errorResponse);
-              throw new HttpException(uri, responseCode, errorResponseContent);
-            }
-            throw new HttpException(uri, responseCode);
-
-          } finally {
-            IOUtils.closeQuietly(errorResponse);
-          }
-        }
-
-        InputStream resultingInputStream;
-        // create the appropriate stream wrapper based on the encoding type
-        if ("gzip".equalsIgnoreCase(encoding)) {
-          resultingInputStream = new GZIPInputStream(connection.getInputStream());
-        } else {
-          resultingInputStream = connection.getInputStream();
-        }
-        return resultingInputStream;
-      }
-    }
-  }
-
-  static class ProxyAuthenticator extends Authenticator {
-    private final PasswordAuthentication auth;
-
-    ProxyAuthenticator(String user, @Nullable String password) {
-      auth = new PasswordAuthentication(user, password == null ? new char[0] : password.toCharArray());
-    }
-
-    @Override
-    protected PasswordAuthentication getPasswordAuthentication() {
-      return auth;
-    }
+  private static SonarException failToDownload(URI uri, IOException e) {
+    throw new SonarException(String.format("Fail to download: %s", uri), e);
   }
 
 }
diff --git a/sonar-core/src/main/java/org/sonar/core/util/HttpsTrust.java b/sonar-core/src/main/java/org/sonar/core/util/HttpsTrust.java
deleted file mode 100644 (file)
index 2fb2b23..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.core.util;
-
-import java.net.HttpURLConnection;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.cert.X509Certificate;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-/**
- * @since 4.0
- */
-class HttpsTrust {
-
-  static final HttpsTrust INSTANCE = new HttpsTrust(new Ssl());
-
-  static class Ssl {
-    SSLSocketFactory newFactory(TrustManager... managers) throws NoSuchAlgorithmException, KeyManagementException {
-      SSLContext context = SSLContext.getInstance("TLS");
-      context.init(null, managers, new SecureRandom());
-      return context.getSocketFactory();
-    }
-  }
-
-  private final SSLSocketFactory socketFactory;
-  private final HostnameVerifier hostnameVerifier;
-
-  HttpsTrust(Ssl context) {
-    this.socketFactory = createSocketFactory(context);
-    this.hostnameVerifier = createHostnameVerifier();
-  }
-
-  void trust(HttpURLConnection connection) {
-    if (connection instanceof HttpsURLConnection) {
-      HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
-      httpsConnection.setSSLSocketFactory(socketFactory);
-      httpsConnection.setHostnameVerifier(hostnameVerifier);
-    }
-  }
-
-  /**
-   * Trust all certificates
-   */
-  private static SSLSocketFactory createSocketFactory(Ssl context) {
-    try {
-      return context.newFactory(new AlwaysTrustManager());
-    } catch (Exception e) {
-      throw new IllegalStateException("Fail to build SSL factory", e);
-    }
-  }
-
-  /**
-   * Trust all hosts
-   */
-  private static HostnameVerifier createHostnameVerifier() {
-    return (hostname, session) -> true;
-  }
-
-  static class AlwaysTrustManager implements X509TrustManager {
-    @Override
-    public X509Certificate[] getAcceptedIssuers() {
-      return new X509Certificate[0];
-    }
-
-    @Override
-    public void checkClientTrusted(X509Certificate[] chain, String authType) {
-      // Do not check
-    }
-
-    @Override
-    public void checkServerTrusted(X509Certificate[] chain, String authType) {
-      // Do not check
-    }
-  }
-}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderIT.java b/sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderIT.java
new file mode 100644 (file)
index 0000000..63e0afc
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.core.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import java.util.zip.GZIPOutputStream;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+import org.simpleframework.http.core.Container;
+import org.simpleframework.http.core.ContainerServer;
+import org.simpleframework.transport.connect.SocketConnection;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.platform.Server;
+import org.sonar.api.utils.SonarException;
+
+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;
+
+public class DefaultHttpDownloaderIT {
+
+  @Rule
+  public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  @Rule
+  public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+  private static SocketConnection socketConnection;
+  private static String baseUrl;
+
+  @BeforeClass
+  public static void startServer() throws IOException {
+    socketConnection = new SocketConnection(new ContainerServer(new Container() {
+      public void handle(Request req, Response resp) {
+        try {
+          if (req.getPath().getPath().contains("/redirect/")) {
+            resp.setCode(303);
+            resp.setValue("Location", "/redirected");
+          } else if (req.getPath().getPath().contains("/timeout/")) {
+            try {
+              Thread.sleep(500);
+              writeDefaultResponse(req, resp);
+            } catch (InterruptedException e) {
+              throw new IllegalStateException(e);
+            }
+          } else if (req.getPath().getPath().contains("/gzip/")) {
+            if (!"gzip".equals(req.getValue("Accept-Encoding"))) {
+              throw new IllegalStateException("Should accept gzip");
+            }
+            resp.setValue("Content-Encoding", "gzip");
+            GZIPOutputStream gzipOutputStream = new GZIPOutputStream(resp.getOutputStream());
+            gzipOutputStream.write("GZIP response".getBytes());
+            gzipOutputStream.close();
+          } else if (req.getPath().getPath().contains("/redirected")) {
+            resp.getPrintStream().append("redirected");
+          } else {
+            writeDefaultResponse(req, resp);
+          }
+
+        } catch (IOException e) {
+          throw new IllegalStateException(e);
+        } finally {
+          try {
+            resp.close();
+          } catch (IOException ignored) {
+          }
+        }
+      }
+    }));
+    SocketAddress address = socketConnection.connect(new InetSocketAddress("localhost", 0));
+
+    baseUrl = String.format("http://%s:%d", ((InetSocketAddress) address).getAddress().getHostAddress(), ((InetSocketAddress) address).getPort());
+  }
+
+  private static PrintStream writeDefaultResponse(Request req, Response resp) throws IOException {
+    return resp.getPrintStream().append("agent=" + req.getValues("User-Agent").get(0));
+  }
+
+  @AfterClass
+  public static void stopServer() throws IOException {
+    if (null != socketConnection) {
+      socketConnection.close();
+    }
+  }
+
+  @Test(timeout = 10000)
+  public void openStream_network_errors() throws IOException, URISyntaxException {
+    // non routable address
+    String url = "http://10.255.255.1";
+
+    assertThatThrownBy(() -> {
+      DefaultHttpDownloader downloader = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig(), 10, 10);
+      downloader.openStream(new URI(url));
+    })
+      .isInstanceOf(SonarException.class)
+      .isEqualToComparingFieldByField(new BaseMatcher<Exception>() {
+        @Override
+        public boolean matches(Object ex) {
+          return ex instanceof SonarException && ((SonarException) ex).getCause() instanceof SocketTimeoutException;
+        }
+
+        @Override
+        public void describeTo(Description arg0) {
+        }
+      });
+  }
+
+  @Test
+  public void downloadBytes() throws URISyntaxException {
+    byte[] bytes = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).readBytes(new URI(baseUrl));
+    assertThat(bytes.length).isGreaterThan(10);
+  }
+
+  @Test
+  public void readString() throws URISyntaxException {
+    String text = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).readString(new URI(baseUrl), StandardCharsets.UTF_8);
+    assertThat(text.length()).isGreaterThan(10);
+  }
+
+  @Test
+  public void readGzipString() throws URISyntaxException {
+    String text = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).readString(new URI(baseUrl + "/gzip/"), StandardCharsets.UTF_8);
+    assertThat(text).isEqualTo("GZIP response");
+  }
+
+  @Test
+  public void readStringWithDefaultTimeout() throws URISyntaxException {
+    String text = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8);
+    assertThat(text.length()).isGreaterThan(10);
+  }
+
+  @Test
+  public void readStringWithTimeout() throws URISyntaxException {
+    assertThatThrownBy(
+      () -> new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig(), null, 50).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8))
+      .isEqualToComparingFieldByField(new BaseMatcher<Exception>() {
+        @Override
+        public boolean matches(Object ex) {
+          return ex instanceof SonarException && ((SonarException) ex).getCause() instanceof SocketTimeoutException;
+        }
+
+        @Override
+        public void describeTo(Description arg0) {
+        }
+      });
+  }
+
+  @Test
+  public void downloadToFile() throws URISyntaxException, IOException {
+    File toDir = temporaryFolder.newFolder();
+    File toFile = new File(toDir, "downloadToFile.txt");
+
+    new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).download(new URI(baseUrl), toFile);
+    assertThat(toFile).exists();
+    assertThat(toFile.length()).isGreaterThan(10L);
+  }
+
+  @Test
+  public void shouldNotCreateFileIfFailToDownload() throws Exception {
+    File toDir = temporaryFolder.newFolder();
+    File toFile = new File(toDir, "downloadToFile.txt");
+
+    try {
+      new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).download(new URI("http://localhost:1"), toFile);
+    } catch (SonarException e) {
+      assertThat(toFile).doesNotExist();
+    }
+  }
+
+  @Test
+  public void userAgent_includes_version_and_SERVER_ID_when_server_is_provided() throws URISyntaxException, IOException {
+    Server server = mock(Server.class);
+    when(server.getVersion()).thenReturn("2.2");
+    MapSettings settings = new MapSettings();
+    settings.setProperty(CoreProperties.SERVER_ID, "blablabla");
+
+    InputStream stream = new DefaultHttpDownloader(server, settings.asConfig()).openStream(new URI(baseUrl));
+    Properties props = new Properties();
+    props.load(stream);
+    stream.close();
+
+    assertThat(props.getProperty("agent")).isEqualTo("SonarQube 2.2 # blablabla");
+  }
+
+  @Test
+  public void userAgent_includes_only_version_when_there_is_no_SERVER_ID_and_server_is_provided() throws URISyntaxException, IOException {
+    Server server = mock(Server.class);
+    when(server.getVersion()).thenReturn("2.2");
+
+    InputStream stream = new DefaultHttpDownloader(server, new MapSettings().asConfig()).openStream(new URI(baseUrl));
+    Properties props = new Properties();
+    props.load(stream);
+    stream.close();
+
+    assertThat(props.getProperty("agent")).isEqualTo("SonarQube 2.2 #");
+  }
+
+  @Test
+  public void followRedirect() throws URISyntaxException {
+    String content = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).readString(new URI(baseUrl + "/redirect/"), StandardCharsets.UTF_8);
+    assertThat(content).isEqualTo("redirected");
+  }
+
+  @Test
+  public void supported_schemes() {
+    assertThat(new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).getSupportedSchemes()).contains("http");
+  }
+
+  @Test
+  public void uri_description() throws URISyntaxException {
+    String description = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).description(new URI("http://sonarsource.org"));
+    assertThat(description).isEqualTo("http://sonarsource.org");
+  }
+
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderTest.java b/sonar-core/src/test/java/org/sonar/core/util/DefaultHttpDownloaderTest.java
deleted file mode 100644 (file)
index e8d5a17..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.core.util;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetSocketAddress;
-import java.net.PasswordAuthentication;
-import java.net.Proxy;
-import java.net.ProxySelector;
-import java.net.SocketAddress;
-import java.net.SocketTimeoutException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Properties;
-import java.util.zip.GZIPOutputStream;
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.TemporaryFolder;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.simpleframework.http.Request;
-import org.simpleframework.http.Response;
-import org.simpleframework.http.core.Container;
-import org.simpleframework.http.core.ContainerServer;
-import org.simpleframework.transport.connect.SocketConnection;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.platform.Server;
-import org.sonar.api.utils.SonarException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-public class DefaultHttpDownloaderTest {
-
-  @Rule
-  public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
-
-  @Rule
-  public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
-  private static SocketConnection socketConnection;
-  private static String baseUrl;
-
-  @BeforeClass
-  public static void startServer() throws IOException {
-    socketConnection = new SocketConnection(new ContainerServer(new Container() {
-      public void handle(Request req, Response resp) {
-        try {
-          if (req.getPath().getPath().contains("/redirect/")) {
-            resp.setCode(303);
-            resp.setValue("Location", "/");
-          } else {
-            if (req.getPath().getPath().contains("/timeout/")) {
-              try {
-                Thread.sleep(500);
-              } catch (InterruptedException e) {
-                throw new IllegalStateException(e);
-              }
-            }
-            if (req.getPath().getPath().contains("/gzip/")) {
-              if (!"gzip".equals(req.getValue("Accept-Encoding"))) {
-                throw new IllegalStateException("Should accept gzip");
-              }
-              resp.setValue("Content-Encoding", "gzip");
-              GZIPOutputStream gzipOutputStream = new GZIPOutputStream(resp.getOutputStream());
-              gzipOutputStream.write("GZIP response".getBytes());
-              gzipOutputStream.close();
-            } else {
-              resp.getPrintStream().append("agent=" + req.getValues("User-Agent").get(0));
-            }
-          }
-        } catch (IOException e) {
-          throw new IllegalStateException(e);
-        } finally {
-          try {
-            resp.close();
-          } catch (IOException ignored) {
-          }
-        }
-      }
-    }));
-    SocketAddress address = socketConnection.connect(new InetSocketAddress("localhost", 0));
-
-    baseUrl = String.format("http://%s:%d", ((InetSocketAddress) address).getAddress().getHostAddress(), ((InetSocketAddress) address).getPort());
-  }
-
-  @AfterClass
-  public static void stopServer() throws IOException {
-    if (null != socketConnection) {
-      socketConnection.close();
-    }
-  }
-
-  @Test(timeout = 10000)
-  public void openStream_network_errors() throws IOException, URISyntaxException {
-    // non routable address
-    String url = "http://10.255.255.1";
-
-    assertThatThrownBy(() -> {
-      DefaultHttpDownloader downloader = new DefaultHttpDownloader(new MapSettings().asConfig(), 10, 50000);
-      downloader.openStream(new URI(url));
-    })
-      .isInstanceOf(SonarException.class)
-      .isEqualToComparingFieldByField(new BaseMatcher<Exception>() {
-        @Override
-        public boolean matches(Object ex) {
-          return ex instanceof SonarException && ((SonarException) ex).getCause() instanceof SocketTimeoutException;
-        }
-
-        @Override
-        public void describeTo(Description arg0) {
-        }
-      });
-  }
-
-  @Test
-  public void downloadBytes() throws URISyntaxException {
-    byte[] bytes = new DefaultHttpDownloader(new MapSettings().asConfig()).readBytes(new URI(baseUrl));
-    assertThat(bytes.length).isGreaterThan(10);
-  }
-
-  @Test
-  public void readString() throws URISyntaxException {
-    String text = new DefaultHttpDownloader(new MapSettings().asConfig()).readString(new URI(baseUrl), StandardCharsets.UTF_8);
-    assertThat(text.length()).isGreaterThan(10);
-  }
-
-  @Test
-  public void readGzipString() throws URISyntaxException {
-    String text = new DefaultHttpDownloader(new MapSettings().asConfig()).readString(new URI(baseUrl + "/gzip/"), StandardCharsets.UTF_8);
-    assertThat(text).isEqualTo("GZIP response");
-  }
-
-  @Test
-  public void readStringWithDefaultTimeout() throws URISyntaxException {
-    String text = new DefaultHttpDownloader(new MapSettings().asConfig()).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8);
-    assertThat(text.length()).isGreaterThan(10);
-  }
-
-  @Test
-  public void readStringWithTimeout() throws URISyntaxException {
-    assertThatThrownBy(() -> new DefaultHttpDownloader(new MapSettings().asConfig(), 50).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8))
-      .isEqualToComparingFieldByField(new BaseMatcher<Exception>() {
-        @Override
-        public boolean matches(Object ex) {
-          return ex instanceof SonarException && ((SonarException) ex).getCause() instanceof SocketTimeoutException;
-        }
-
-        @Override
-        public void describeTo(Description arg0) {
-        }
-      });
-  }
-
-  @Test
-  public void downloadToFile() throws URISyntaxException, IOException {
-    File toDir = temporaryFolder.newFolder();
-    File toFile = new File(toDir, "downloadToFile.txt");
-
-    new DefaultHttpDownloader(new MapSettings().asConfig()).download(new URI(baseUrl), toFile);
-    assertThat(toFile).exists();
-    assertThat(toFile.length()).isGreaterThan(10L);
-  }
-
-  @Test
-  public void shouldNotCreateFileIfFailToDownload() throws Exception {
-    File toDir = temporaryFolder.newFolder();
-    File toFile = new File(toDir, "downloadToFile.txt");
-
-    try {
-      int port = new InetSocketAddress("localhost", 0).getPort();
-      new DefaultHttpDownloader(new MapSettings().asConfig()).download(new URI("http://localhost:" + port), toFile);
-    } catch (SonarException e) {
-      assertThat(toFile).doesNotExist();
-    }
-  }
-
-  @Test
-  public void userAgent_includes_version_and_SERVER_ID_when_server_is_provided() throws URISyntaxException, IOException {
-    Server server = mock(Server.class);
-    when(server.getVersion()).thenReturn("2.2");
-    MapSettings settings = new MapSettings();
-    settings.setProperty(CoreProperties.SERVER_ID, "blablabla");
-
-    InputStream stream = new DefaultHttpDownloader(server, settings.asConfig()).openStream(new URI(baseUrl));
-    Properties props = new Properties();
-    props.load(stream);
-    stream.close();
-
-    assertThat(props.getProperty("agent")).isEqualTo("SonarQube 2.2 # blablabla");
-  }
-
-  @Test
-  public void userAgent_includes_only_version_when_there_is_no_SERVER_ID_and_server_is_provided() throws URISyntaxException, IOException {
-    Server server = mock(Server.class);
-    when(server.getVersion()).thenReturn("2.2");
-
-    InputStream stream = new DefaultHttpDownloader(server, new MapSettings().asConfig()).openStream(new URI(baseUrl));
-    Properties props = new Properties();
-    props.load(stream);
-    stream.close();
-
-    assertThat(props.getProperty("agent")).isEqualTo("SonarQube 2.2 #");
-  }
-
-  @Test
-  public void userAgent_is_static_value_when_server_is_not_provided() throws URISyntaxException, IOException {
-    InputStream stream = new DefaultHttpDownloader(new MapSettings().asConfig()).openStream(new URI(baseUrl));
-    Properties props = new Properties();
-    props.load(stream);
-    stream.close();
-
-    assertThat(props.getProperty("agent")).isEqualTo("SonarQube");
-  }
-
-  @Test
-  public void followRedirect() throws URISyntaxException {
-    String content = new DefaultHttpDownloader(new MapSettings().asConfig()).readString(new URI(baseUrl + "/redirect/"), StandardCharsets.UTF_8);
-    assertThat(content).contains("agent");
-  }
-
-  @Test
-  public void shouldGetDirectProxySynthesis() throws URISyntaxException {
-    ProxySelector proxySelector = mock(ProxySelector.class);
-    when(proxySelector.select(any(URI.class))).thenReturn(Arrays.asList(Proxy.NO_PROXY));
-    assertThat(DefaultHttpDownloader.BaseHttpDownloader.getProxySynthesis(new URI("http://an_url"), proxySelector)).isEqualTo("no proxy");
-  }
-
-  @Test
-  public void shouldGetProxySynthesis() throws URISyntaxException {
-    ProxySelector proxySelector = mock(ProxySelector.class);
-    when(proxySelector.select(any(URI.class))).thenReturn(Arrays.asList(new FakeProxy()));
-    assertThat(DefaultHttpDownloader.BaseHttpDownloader.getProxySynthesis(new URI("http://an_url"), proxySelector)).isEqualTo("HTTP proxy: /123.45.67.89:4040");
-  }
-
-  @Test
-  public void supported_schemes() {
-    assertThat(new DefaultHttpDownloader(new MapSettings().asConfig()).getSupportedSchemes()).contains("http");
-  }
-
-  @Test
-  public void uri_description() throws URISyntaxException {
-    String description = new DefaultHttpDownloader(new MapSettings().asConfig()).description(new URI("http://sonarsource.org"));
-    assertThat(description).matches("http://sonarsource.org \\(.*\\)");
-  }
-
-  @Test
-  public void configure_http_proxy_credentials() {
-    DefaultHttpDownloader.AuthenticatorFacade system = mock(DefaultHttpDownloader.AuthenticatorFacade.class);
-    MapSettings settings = new MapSettings();
-    settings.setProperty("https.proxyHost", "1.2.3.4");
-    settings.setProperty("http.proxyUser", "the_login");
-    settings.setProperty("http.proxyPassword", "the_passwd");
-
-    new DefaultHttpDownloader.BaseHttpDownloader(system, settings.asConfig(), null);
-
-    verify(system).setDefaultAuthenticator(argThat(authenticator -> {
-      DefaultHttpDownloader.ProxyAuthenticator a = (DefaultHttpDownloader.ProxyAuthenticator) authenticator;
-      PasswordAuthentication authentication = a.getPasswordAuthentication();
-      return authentication.getUserName().equals("the_login") &&
-        new String(authentication.getPassword()).equals("the_passwd");
-    }));
-  }
-
-  private static class FakeProxy extends Proxy {
-    FakeProxy() {
-      super(Type.HTTP, new InetSocketAddress("123.45.67.89", 4040));
-    }
-  }
-}
diff --git a/sonar-core/src/test/java/org/sonar/core/util/HttpsTrustTest.java b/sonar-core/src/test/java/org/sonar/core/util/HttpsTrustTest.java
deleted file mode 100644 (file)
index 85e1f57..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.core.util;
-
-import java.io.IOException;
-import java.net.URL;
-import java.security.KeyManagementException;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.TrustManager;
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class HttpsTrustTest {
-  @Test
-  public void trustAllHosts() throws Exception {
-    HttpsURLConnection connection = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection);
-
-    assertThat(connection.getHostnameVerifier()).isNotNull();
-    assertThat(connection.getHostnameVerifier().verify("foo", null)).isTrue();
-  }
-
-  @Test
-  public void singleHostnameVerifier() throws Exception {
-    HttpsURLConnection connection1 = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection1);
-    HttpsURLConnection connection2 = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection2);
-
-    assertThat(connection1.getHostnameVerifier()).isSameAs(connection2.getHostnameVerifier());
-  }
-
-  @Test
-  public void trustAllCerts() throws Exception {
-    HttpsURLConnection connection1 = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection1);
-
-    assertThat(connection1.getSSLSocketFactory()).isNotNull();
-    assertThat(connection1.getSSLSocketFactory().getDefaultCipherSuites()).isNotEmpty();
-  }
-
-  @Test
-  public void singleSslFactory() throws Exception {
-    HttpsURLConnection connection1 = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection1);
-    HttpsURLConnection connection2 = newHttpsConnection();
-    HttpsTrust.INSTANCE.trust(connection2);
-
-    assertThat(connection1.getSSLSocketFactory()).isSameAs(connection2.getSSLSocketFactory());
-  }
-
-  @Test
-  public void testAlwaysTrustManager() {
-    HttpsTrust.AlwaysTrustManager manager = new HttpsTrust.AlwaysTrustManager();
-    assertThat(manager.getAcceptedIssuers()).isEmpty();
-    // does nothing
-    manager.checkClientTrusted(null, null);
-    manager.checkServerTrusted(null, null);
-  }
-
-  @Test
-  public void failOnError() throws Exception {
-    HttpsTrust.Ssl context = mock(HttpsTrust.Ssl.class);
-    KeyManagementException cause = new KeyManagementException("foo");
-    when(context.newFactory(any(TrustManager.class))).thenThrow(cause);
-
-    try {
-      new HttpsTrust(context);
-      fail();
-    } catch (IllegalStateException e) {
-      assertThat(e.getMessage()).isEqualTo("Fail to build SSL factory");
-      assertThat(e.getCause()).isSameAs(cause);
-    }
-  }
-
-  private HttpsURLConnection newHttpsConnection() throws IOException {
-    return (HttpsURLConnection) new URL("https://localhost").openConnection();
-  }
-}