diff options
author | Wojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com> | 2023-06-15 07:32:29 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-20 13:10:17 +0000 |
commit | e8305b73e2ebc8fa5c555a88bf9a9f94f4a135c2 (patch) | |
tree | 2dc40ae7f6639a36a1fbc88cea92fee992f20693 /sonar-core/src/main | |
parent | 1235c887b45a26e03824a1d49aa4a0bf7c0ad119 (diff) | |
download | sonarqube-e8305b73e2ebc8fa5c555a88bf9a9f94f4a135c2.tar.gz sonarqube-e8305b73e2ebc8fa5c555a88bf9a9f94f4a135c2.zip |
SONAR-19597 Security fix (SSF-420)
Diffstat (limited to 'sonar-core/src/main')
-rw-r--r-- | sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java | 248 | ||||
-rw-r--r-- | sonar-core/src/main/java/org/sonar/core/util/HttpsTrust.java | 99 |
2 files changed, 41 insertions, 306 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java b/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java index 85c56de88be..8db9479e61d 100644 --- a/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java +++ b/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java @@ -19,35 +19,25 @@ */ 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 index 2fb2b235c92..00000000000 --- a/sonar-core/src/main/java/org/sonar/core/util/HttpsTrust.java +++ /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 - } - } -} |