@@ -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' |
@@ -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); | |||
} | |||
} |
@@ -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 | |||
} | |||
} | |||
} |
@@ -22,16 +22,13 @@ 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.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; | |||
@@ -56,18 +53,14 @@ 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 { | |||
public class DefaultHttpDownloaderIT { | |||
@Rule | |||
public TemporaryFolder temporaryFolder = new TemporaryFolder(); | |||
@Rule | |||
public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); | |||
@@ -81,27 +74,28 @@ public class DefaultHttpDownloaderTest { | |||
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); | |||
} | |||
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); | |||
} | |||
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)); | |||
} 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 { | |||
@@ -117,6 +111,10 @@ public class DefaultHttpDownloaderTest { | |||
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) { | |||
@@ -130,7 +128,7 @@ public class DefaultHttpDownloaderTest { | |||
String url = "http://10.255.255.1"; | |||
assertThatThrownBy(() -> { | |||
DefaultHttpDownloader downloader = new DefaultHttpDownloader(new MapSettings().asConfig(), 10, 50000); | |||
DefaultHttpDownloader downloader = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig(), 10, 10); | |||
downloader.openStream(new URI(url)); | |||
}) | |||
.isInstanceOf(SonarException.class) | |||
@@ -148,31 +146,32 @@ public class DefaultHttpDownloaderTest { | |||
@Test | |||
public void downloadBytes() throws URISyntaxException { | |||
byte[] bytes = new DefaultHttpDownloader(new MapSettings().asConfig()).readBytes(new URI(baseUrl)); | |||
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(new MapSettings().asConfig()).readString(new URI(baseUrl), StandardCharsets.UTF_8); | |||
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(new MapSettings().asConfig()).readString(new URI(baseUrl + "/gzip/"), StandardCharsets.UTF_8); | |||
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(new MapSettings().asConfig()).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8); | |||
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(new MapSettings().asConfig(), 50).readString(new URI(baseUrl + "/timeout/"), StandardCharsets.UTF_8)) | |||
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) { | |||
@@ -190,7 +189,7 @@ public class DefaultHttpDownloaderTest { | |||
File toDir = temporaryFolder.newFolder(); | |||
File toFile = new File(toDir, "downloadToFile.txt"); | |||
new DefaultHttpDownloader(new MapSettings().asConfig()).download(new URI(baseUrl), toFile); | |||
new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).download(new URI(baseUrl), toFile); | |||
assertThat(toFile).exists(); | |||
assertThat(toFile.length()).isGreaterThan(10L); | |||
} | |||
@@ -201,8 +200,7 @@ public class DefaultHttpDownloaderTest { | |||
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); | |||
new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).download(new URI("http://localhost:1"), toFile); | |||
} catch (SonarException e) { | |||
assertThat(toFile).doesNotExist(); | |||
} | |||
@@ -236,68 +234,21 @@ public class DefaultHttpDownloaderTest { | |||
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"); | |||
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(new MapSettings().asConfig()).getSupportedSchemes()).contains("http"); | |||
assertThat(new DefaultHttpDownloader(mock(Server.class), 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"); | |||
})); | |||
String description = new DefaultHttpDownloader(mock(Server.class), new MapSettings().asConfig()).description(new URI("http://sonarsource.org")); | |||
assertThat(description).isEqualTo("http://sonarsource.org"); | |||
} | |||
private static class FakeProxy extends Proxy { | |||
FakeProxy() { | |||
super(Type.HTTP, new InetSocketAddress("123.45.67.89", 4040)); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |