Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/2114[#2114]: Fix false "Cygwin is not supported" warning in Git Bash
* https://github.com/devonfw/IDEasy/issues/865[#865]: az not working on Mac
* https://github.com/devonfw/IDEasy/issues/2026[#2026]: Create UvRepository and UvBasedCommandlet
* https://github.com/devonfw/IDEasy/issues/2137[#2137]: Fix `fix-vpn-tls-problem` not detecting TLS certificate issues behind HTTP redirects and surface an actionable hint (failing URL + fix command) when a download fails due to a certificate trust error

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/46?closed=1[milestone 2026.07.001].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -87,18 +86,16 @@ protected void doRun() {
throw new CliException("Invalid target URL/host '" + endpointInput + "': " + e.getMessage(), e);
}

String host = endpoint.host();
int port = endpoint.port();
Path customTruststorePath = this.context.getUserHomeIde().resolve("truststore").resolve("truststore.p12");

if (TruststoreUtil.isTruststorePresent(customTruststorePath) && TruststoreUtil.isReachable(host, port, customTruststorePath)) {
if (TruststoreUtil.isTruststorePresent(customTruststorePath) && TruststoreUtil.isReachable(endpoint, customTruststorePath)) {
IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath);
configureIdeOptions(customTruststorePath);
return;
}

if (TruststoreUtil.isReachable(host, port)) {
IdeLogLevel.SUCCESS.log(LOG, "Successfully connected to {}:{} without certificate changes.", host, port);
if (TruststoreUtil.isReachable(endpoint, null)) {
IdeLogLevel.SUCCESS.log(LOG, "Successfully connected to {}:{} without certificate changes.", endpoint.host(), endpoint.port());
LOG.info("No truststore update is required for the given address.");
if (defaultUrlUsed) {
LOG.info(
Expand All @@ -108,19 +105,22 @@ protected void doRun() {
return;
}

LOG.info("The given address {}:{} is not reachable/valid without certificate changes. Continuing with certificate capture.", host, port);
LOG.info("The given address {} is not reachable/valid without certificate changes. Continuing with certificate capture.", endpoint.url());

// download URLs are frequently redirected (e.g. https://aka.ms/...), so capture the certificate from the effective endpoint after following all redirects
TruststoreUtil.TlsEndpoint effectiveEndpoint = TruststoreUtil.resolveEffectiveEndpoint(endpoint);

X509Certificate certificate;
try {
certificate = TruststoreUtil.fetchServerCertificate(host, port);
certificate = TruststoreUtil.fetchServerCertificate(effectiveEndpoint.host(), effectiveEndpoint.port());
} catch (Exception e) {
LOG.error("Failed to capture certificate from {}:{}.", host, port, e);
LOG.error("Failed to capture certificate from {}:{}.", effectiveEndpoint.host(), effectiveEndpoint.port(), e);
IdeLogLevel.INTERACTION.log(LOG,
"Please check proxy/VPN and retry. You can also follow: https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues");
return;
}

LOG.info("Captured untrusted certificate:");
LOG.info("Captured untrusted certificate from {}:{}:", effectiveEndpoint.host(), effectiveEndpoint.port());
LOG.info(TruststoreUtil.describeCertificate(certificate));

boolean addToTruststore = this.context.question("Do you want to add this certificate to the custom truststore at {}?", customTruststorePath);
Expand All @@ -140,7 +140,7 @@ protected void doRun() {

configureIdeOptions(customTruststorePath);

if (TruststoreUtil.isReachable(host, port, customTruststorePath)) {
if (TruststoreUtil.isReachable(endpoint, customTruststorePath)) {
IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with custom truststore.");
} else {
LOG.warn("TLS handshake still fails even with custom truststore.");
Expand Down
44 changes: 44 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/io/HttpClientFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.devonfw.tools.ide.io;

import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;

import javax.net.ssl.SSLContext;

/**
* Factory for {@link HttpClient} instances that are configured consistently across IDEasy. All HTTP communication (tool and plugin downloads, reachability
* checks, URL updates, ...) has to {@link Redirect#ALWAYS follow redirects} because download URLs are frequently shortened or redirected (e.g.
* {@code https://aka.ms/...}). Centralizing the configuration here ensures that no code-path accidentally skips redirect handling.
*/
public final class HttpClientFactory {

private HttpClientFactory() {
// utility class
}

/**
* @return a new {@link HttpClient.Builder} that {@link Redirect#ALWAYS always follows redirects}.
*/
public static HttpClient.Builder createBuilder() {
return HttpClient.newBuilder().followRedirects(Redirect.ALWAYS);
}

/**
* @return a new redirect-following {@link HttpClient} using the default TLS configuration.
*/
public static HttpClient create() {
return createBuilder().build();
}

/**
* @param sslContext the {@link SSLContext} to use for TLS connections or {@code null} to use the default configuration.
* @return a new redirect-following {@link HttpClient} using the given {@link SSLContext}.
*/
public static HttpClient create(SSLContext sslContext) {
HttpClient.Builder builder = createBuilder();
if (sslContext != null) {
builder.sslContext(sslContext);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
Expand Down Expand Up @@ -42,7 +41,7 @@ public abstract class HttpDownloader {

protected static HttpClient createHttpClient() {

return HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).build();
return HttpClientFactory.create();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void logStatusMessage() {
LOG.error(error.toString());
}
if (isTlsTrustIssue(error)) {
logTruststoreFixHint();
logTruststoreFixHint(this.onlineCheckUrl);
} else {
IdeLogLevel.INTERACTION.log(LOG, "Please check potential proxy settings, ensure you are properly connected to the internet and retry this operation.");
}
Expand All @@ -131,22 +131,30 @@ public <T> T invokeNetworkTask(Callable<T> callable, String uri) {
configureNetworkProxy();
try {
return callable.call();
} catch (IOException e) {
this.onlineCheck.set(e);
} catch (Exception e) {
if (e instanceof IOException ioException) {
this.onlineCheck.set(ioException);
}
// the underlying SSLHandshakeException is often wrapped (e.g. as an IllegalStateException by the downloader), hence we scan the entire cause chain and
// must not rely on the exception being an IOException.
if (isTlsTrustIssue(e)) {
logTruststoreFixHint();
logTruststoreFixHint(uri);
throw new IllegalStateException("TLS certificate trust error whilst communicating to " + uri, e);
}
throw new IllegalStateException("Network error whilst communicating to " + uri, e);
} catch (Exception e) {
throw new IllegalStateException("Unexpected checked exception whilst communicating to " + uri, e);
if (e instanceof IOException) {
throw new IllegalStateException("Network error whilst communicating to " + uri, e);
}
throw new IllegalStateException("Unexpected error whilst communicating to " + uri, e);
}
}

private void logTruststoreFixHint() {
private void logTruststoreFixHint(String uri) {

LOG.warn(
"You are having TLS trust issues (PKIX/certificate-path/SSL handshake). As a workaround you can create and configure a truststore via the following command (replace <url> with the failing endpoint):\nide fix-vpn-tls-problem <url>");
IdeLogLevel.INTERACTION.log(LOG, "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues");
LOG.warn("The TLS connection to {} failed due to a certificate trust error (PKIX / certificate-path / SSL handshake). "
+ "This commonly happens behind a corporate VPN or proxy that intercepts TLS traffic.", uri);
LOG.warn("Please first verify that the URL above is a legitimate endpoint that you trust and that you are reaching it securely. "
+ "If it is trustworthy, you can register its certificate in a custom truststore by running:\nide fix-vpn-tls-problem {}", uri);
IdeLogLevel.INTERACTION.log(LOG, "For more details see: https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues");
}

boolean isTlsTrustIssue(Throwable throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
Expand All @@ -19,6 +18,7 @@

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.HttpClientFactory;
import com.devonfw.tools.ide.os.MacOsHelper;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.step.Step;
Expand Down Expand Up @@ -145,7 +145,7 @@ private String getFileExtensionFromUrl(String urlString) throws RuntimeException

URI uri = null;
HttpRequest request;
try (HttpClient client = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).build()) {
try (HttpClient client = HttpClientFactory.create()) {
uri = URI.create(urlString);
request = HttpRequest.newBuilder().uri(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody()).timeout(Duration.ofSeconds(5)).build();
Expand Down
Loading
Loading