From 5b8f1d5390e84c13367760b09b2f27af48d24fd9 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Fri, 13 Mar 2026 16:30:09 +0100 Subject: [PATCH 01/22] #1750: added Updater for golang support --- .../tools/ide/url/tool/go/GoUrlUpdater.java | 49 +++++++++++++++++++ .../tools/ide/url/updater/UpdateManager.java | 3 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java new file mode 100644 index 000000000..adb391262 --- /dev/null +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java @@ -0,0 +1,49 @@ +package com.devonfw.tools.ide.url.tool.go; + +import com.devonfw.tools.ide.url.model.folder.UrlVersion; +import com.devonfw.tools.ide.url.updater.GithubUrlTagUpdater; + +/** + * {@link GithubUrlTagUpdater} for Go programming language. + */ +public class GoUrlUpdater extends GithubUrlTagUpdater { + + private static final String GO_BASE_URL = "https://go.dev/dl/"; + + @Override + public String getTool() { + return "go"; + } + + @Override + protected String getGithubOrganization() { + return "golang"; + } + + @Override + protected String getGithubRepository() { + return "go"; + } + + @Override + protected String getVersionPrefixToRemove() { + return "go"; + } + + @Override + protected String getDownloadBaseUrl() { + return GO_BASE_URL; + } + + @Override + protected void addVersion(UrlVersion urlVersion) { + String baseUrl = getDownloadBaseUrl() + "go${version}."; + doAddVersion(urlVersion, baseUrl + "windows-amd64.zip", WINDOWS, X64); + doAddVersion(urlVersion, baseUrl + "windows-arm64.zip", WINDOWS, ARM64); + doAddVersion(urlVersion, baseUrl + "linux-amd64.tar.gz", LINUX, X64); + doAddVersion(urlVersion, baseUrl + "linux-arm64.tar.gz", LINUX, ARM64); + doAddVersion(urlVersion, baseUrl + "darwin-amd64.tar.gz", MAC, X64); + doAddVersion(urlVersion, baseUrl + "darwin-arm64.tar.gz", MAC, ARM64); + } +} + diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java index ea32626dd..9b380387e 100644 --- a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java @@ -22,6 +22,7 @@ import com.devonfw.tools.ide.url.tool.gcloud.GCloudUrlUpdater; import com.devonfw.tools.ide.url.tool.gcviewer.GcViewerUrlUpdater; import com.devonfw.tools.ide.url.tool.gh.GhUrlUpdater; +import com.devonfw.tools.ide.url.tool.go.GoUrlUpdater; import com.devonfw.tools.ide.url.tool.graalvm.GraalVmCommunityUpdater; import com.devonfw.tools.ide.url.tool.graalvm.GraalVmOracleUrlUpdater; import com.devonfw.tools.ide.url.tool.gradle.GradleUrlUpdater; @@ -73,7 +74,7 @@ public class UpdateManager extends AbstractProcessorWithTimeout { new KotlincNativeUrlUpdater(), new LazyDockerUrlUpdater(), new MvnUrlUpdater(), new Mvn4UrlUpdater(), new NgUrlUpdater(), new NodeUrlUpdater(), new NpmUrlUpdater(), new OcUrlUpdater(), new PgAdminUrlUpdater(), new PipUrlUpdater(), new PycharmUrlUpdater(), new PythonUrlUpdater(), new QuarkusUrlUpdater(), new DockerRancherDesktopUrlUpdater(), new SonarUrlUpdater(), - new TerraformUrlUpdater(), new TomcatUrlUpdater(), new UvUrlUpdater(), new VsCodeUrlUpdater()); + new TerraformUrlUpdater(), new TomcatUrlUpdater(), new UvUrlUpdater(), new VsCodeUrlUpdater(), new GoUrlUpdater()); /** * The constructor. From 4f72ebe161ddd8d7430c31b830de761d27597f72 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 16 Mar 2026 08:37:34 +0100 Subject: [PATCH 02/22] #1751: create Go commandlet --- .../devonfw/tools/ide/tool/go/java/Go.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java new file mode 100644 index 000000000..1f65eb377 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java @@ -0,0 +1,30 @@ +package com.devonfw.tools.ide.tool.go.java; + +import java.util.Set; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; + +/** + * {@link ToolCommandlet} for the Go programming language. + */ +public class Go extends LocalToolCommandlet { + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public Go(IdeContext context) { + + super(context, "go", Set.of(Tag.JAVA, Tag.RUNTIME)); + } + + @Override + public String getToolHelpArguments() { + + return "--help"; + } +} From 16b6a0e9b69edbb89cd4c4cca17ccb7df45ebbdc Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 16 Mar 2026 15:30:52 +0100 Subject: [PATCH 03/22] #1751: remove wrongly committed files --- .../devonfw/tools/ide/tool/go/java/Go.java | 3 +- .../tools/ide/url/tool/go/GoUrlUpdater.java | 49 ------------------- .../tools/ide/url/updater/UpdateManager.java | 3 +- 3 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java index 1f65eb377..e36211dc4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java @@ -25,6 +25,7 @@ public Go(IdeContext context) { @Override public String getToolHelpArguments() { - return "--help"; + return "help"; } + } diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java deleted file mode 100644 index adb391262..000000000 --- a/url-updater/src/main/java/com/devonfw/tools/ide/url/tool/go/GoUrlUpdater.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.devonfw.tools.ide.url.tool.go; - -import com.devonfw.tools.ide.url.model.folder.UrlVersion; -import com.devonfw.tools.ide.url.updater.GithubUrlTagUpdater; - -/** - * {@link GithubUrlTagUpdater} for Go programming language. - */ -public class GoUrlUpdater extends GithubUrlTagUpdater { - - private static final String GO_BASE_URL = "https://go.dev/dl/"; - - @Override - public String getTool() { - return "go"; - } - - @Override - protected String getGithubOrganization() { - return "golang"; - } - - @Override - protected String getGithubRepository() { - return "go"; - } - - @Override - protected String getVersionPrefixToRemove() { - return "go"; - } - - @Override - protected String getDownloadBaseUrl() { - return GO_BASE_URL; - } - - @Override - protected void addVersion(UrlVersion urlVersion) { - String baseUrl = getDownloadBaseUrl() + "go${version}."; - doAddVersion(urlVersion, baseUrl + "windows-amd64.zip", WINDOWS, X64); - doAddVersion(urlVersion, baseUrl + "windows-arm64.zip", WINDOWS, ARM64); - doAddVersion(urlVersion, baseUrl + "linux-amd64.tar.gz", LINUX, X64); - doAddVersion(urlVersion, baseUrl + "linux-arm64.tar.gz", LINUX, ARM64); - doAddVersion(urlVersion, baseUrl + "darwin-amd64.tar.gz", MAC, X64); - doAddVersion(urlVersion, baseUrl + "darwin-arm64.tar.gz", MAC, ARM64); - } -} - diff --git a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java index 9b380387e..ea32626dd 100644 --- a/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java +++ b/url-updater/src/main/java/com/devonfw/tools/ide/url/updater/UpdateManager.java @@ -22,7 +22,6 @@ import com.devonfw.tools.ide.url.tool.gcloud.GCloudUrlUpdater; import com.devonfw.tools.ide.url.tool.gcviewer.GcViewerUrlUpdater; import com.devonfw.tools.ide.url.tool.gh.GhUrlUpdater; -import com.devonfw.tools.ide.url.tool.go.GoUrlUpdater; import com.devonfw.tools.ide.url.tool.graalvm.GraalVmCommunityUpdater; import com.devonfw.tools.ide.url.tool.graalvm.GraalVmOracleUrlUpdater; import com.devonfw.tools.ide.url.tool.gradle.GradleUrlUpdater; @@ -74,7 +73,7 @@ public class UpdateManager extends AbstractProcessorWithTimeout { new KotlincNativeUrlUpdater(), new LazyDockerUrlUpdater(), new MvnUrlUpdater(), new Mvn4UrlUpdater(), new NgUrlUpdater(), new NodeUrlUpdater(), new NpmUrlUpdater(), new OcUrlUpdater(), new PgAdminUrlUpdater(), new PipUrlUpdater(), new PycharmUrlUpdater(), new PythonUrlUpdater(), new QuarkusUrlUpdater(), new DockerRancherDesktopUrlUpdater(), new SonarUrlUpdater(), - new TerraformUrlUpdater(), new TomcatUrlUpdater(), new UvUrlUpdater(), new VsCodeUrlUpdater(), new GoUrlUpdater()); + new TerraformUrlUpdater(), new TomcatUrlUpdater(), new UvUrlUpdater(), new VsCodeUrlUpdater()); /** * The constructor. From e2da97032a5c30d8a9d573612a83372c3dd748d7 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 16 Mar 2026 15:49:40 +0100 Subject: [PATCH 04/22] #1751: added Go tag, fixed naming and added go to the commandletManger --- .../devonfw/tools/ide/commandlet/CommandletManagerImpl.java | 2 ++ cli/src/main/java/com/devonfw/tools/ide/common/Tag.java | 3 +++ .../java/com/devonfw/tools/ide/tool/go/{java => }/Go.java | 6 +++--- 3 files changed, 8 insertions(+), 3 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/tool/go/{java => }/Go.java (83%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index ed9083e5a..c44f5b740 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -27,6 +27,7 @@ import com.devonfw.tools.ide.tool.eclipse.Eclipse; import com.devonfw.tools.ide.tool.gcviewer.GcViewer; import com.devonfw.tools.ide.tool.gh.Gh; +import com.devonfw.tools.ide.tool.go.Go; import com.devonfw.tools.ide.tool.graalvm.GraalVm; import com.devonfw.tools.ide.tool.gradle.Gradle; import com.devonfw.tools.ide.tool.helm.Helm; @@ -145,6 +146,7 @@ public CommandletManagerImpl(IdeContext context) { add(new Yarn(context)); add(new Corepack(context)); add(new Pip(context)); + add(new Go(context)); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/Tag.java b/cli/src/main/java/com/devonfw/tools/ide/common/Tag.java index 5aa67e317..6ae92c6e0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/common/Tag.java +++ b/cli/src/main/java/com/devonfw/tools/ide/common/Tag.java @@ -39,6 +39,9 @@ public final class Tag { /** {@link Tag} for Java. */ public static final Tag JAVA = create("java", JVM); + /** {@link Tag} for Go. */ + public static final Tag GO = create("go", LANGUAGE); + /** {@link Tag} for Kotlin. */ public static final Tag KOTLIN = create("kotlin", JVM); diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java similarity index 83% rename from cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java rename to cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java index e36211dc4..d3b786364 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/go/java/Go.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.tool.go.java; +package com.devonfw.tools.ide.tool.go; import java.util.Set; @@ -19,7 +19,7 @@ public class Go extends LocalToolCommandlet { */ public Go(IdeContext context) { - super(context, "go", Set.of(Tag.JAVA, Tag.RUNTIME)); + super(context, "go", Set.of(Tag.GO)); } @Override @@ -27,5 +27,5 @@ public String getToolHelpArguments() { return "help"; } - + } From 2e39d2fbda7986b23062a7ccebee528449fc74bf Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Tue, 17 Mar 2026 09:12:04 +0100 Subject: [PATCH 05/22] #1751: added tool installation for go --- .../com/devonfw/tools/ide/tool/go/Go.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java index d3b786364..a412ee4ff 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java @@ -1,17 +1,28 @@ package com.devonfw.tools.ide.tool.go; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.tool.ToolInstallRequest; +import com.devonfw.tools.ide.version.VersionIdentifier; /** * {@link ToolCommandlet} for the Go programming language. */ public class Go extends LocalToolCommandlet { + private static final Logger LOG = LoggerFactory.getLogger(Go.class); + /** * The constructor. * @@ -28,4 +39,65 @@ public String getToolHelpArguments() { return "help"; } + @Override + protected void performToolInstallation(ToolInstallRequest request, Path installationPath) { + + FileAccess fileAccess = this.context.getFileAccess(); + VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion(); + + // Backup existing installation if present + if (Files.isDirectory(installationPath)) { + fileAccess.backup(installationPath); + } + + // Download and extract the Go source archive + Path downloadedToolFile = downloadTool(request.getRequested().getEdition().edition(), resolvedVersion); + fileAccess.mkdirs(installationPath.getParent()); + fileAccess.extract(downloadedToolFile, installationPath, this::postExtract, true); + + // Build Go from source by running make.bash + buildGoFromSource(installationPath); + + // Write version file + this.context.writeVersionFile(resolvedVersion, installationPath); + LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath); + } + + /** + * Builds Go from source by executing the make.bash script located in the go/src directory. + * + * @param installationPath the {@link Path} where Go source has been extracted. + */ + private void buildGoFromSource(Path installationPath) { + + Path goSrcDir = installationPath.resolve("src"); + if (!Files.isDirectory(goSrcDir)) { + throw new IllegalStateException("Go source directory not found at " + goSrcDir); + } + + Path makeScript = goSrcDir.resolve("make.bash"); + if (!Files.exists(makeScript)) { + throw new IllegalStateException("make.bash script not found at " + makeScript); + } + + LOG.debug("Building Go from source using make.bash at {}", makeScript); + + if (this.context.getSystemInfo().isWindows()) { + // On Windows, execute make.bash via bash since it's a POSIX shell script + Path bash = this.context.findBashRequired(); + this.context.newProcess() + .executable(bash) + .addArgs("-c", "cd \"" + goSrcDir + "\" && ./make.bash") + .run(ProcessMode.DEFAULT); + } else { + // On Linux/macOS, execute make.bash directly + this.context.newProcess() + .executable(makeScript) + .directory(goSrcDir) + .run(ProcessMode.DEFAULT); + } + + LOG.debug("Go build completed successfully"); + } + } From 0fe2b3d2cf8e084c849be49b31096940ea6ab458 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Wed, 18 Mar 2026 08:55:31 +0100 Subject: [PATCH 06/22] #1751: implemented go installation --- .../com/devonfw/tools/ide/tool/go/Go.java | 64 ++++--------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java index a412ee4ff..9ff272d7e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java @@ -9,12 +9,10 @@ import com.devonfw.tools.ide.common.Tag; import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.ToolCommandlet; import com.devonfw.tools.ide.tool.ToolInstallRequest; -import com.devonfw.tools.ide.version.VersionIdentifier; /** * {@link ToolCommandlet} for the Go programming language. @@ -41,63 +39,29 @@ public String getToolHelpArguments() { @Override protected void performToolInstallation(ToolInstallRequest request, Path installationPath) { - - FileAccess fileAccess = this.context.getFileAccess(); - VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion(); - - // Backup existing installation if present - if (Files.isDirectory(installationPath)) { - fileAccess.backup(installationPath); - } - - // Download and extract the Go source archive - Path downloadedToolFile = downloadTool(request.getRequested().getEdition().edition(), resolvedVersion); - fileAccess.mkdirs(installationPath.getParent()); - fileAccess.extract(downloadedToolFile, installationPath, this::postExtract, true); - - // Build Go from source by running make.bash - buildGoFromSource(installationPath); - - // Write version file - this.context.writeVersionFile(resolvedVersion, installationPath); - LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath); + super.performToolInstallation(request, installationPath); + runGoBootstrapIfPresent(installationPath); } - /** - * Builds Go from source by executing the make.bash script located in the go/src directory. - * - * @param installationPath the {@link Path} where Go source has been extracted. - */ - private void buildGoFromSource(Path installationPath) { - - Path goSrcDir = installationPath.resolve("src"); - if (!Files.isDirectory(goSrcDir)) { - throw new IllegalStateException("Go source directory not found at " + goSrcDir); + private void runGoBootstrapIfPresent(Path installationPath) { + Path makeBash = installationPath.resolve("make.bash"); + Path workingDir = installationPath; + if (!Files.isRegularFile(makeBash)) { + workingDir = installationPath.resolve("src"); + makeBash = workingDir.resolve("make.bash"); } - - Path makeScript = goSrcDir.resolve("make.bash"); - if (!Files.exists(makeScript)) { - throw new IllegalStateException("make.bash script not found at " + makeScript); + if (!Files.isRegularFile(makeBash)) { + LOG.debug("No make.bash found in {} or {} - skipping source bootstrap.", installationPath, installationPath.resolve("src")); + return; } - LOG.debug("Building Go from source using make.bash at {}", makeScript); - + LOG.info("Running Go bootstrap script {}", makeBash); if (this.context.getSystemInfo().isWindows()) { - // On Windows, execute make.bash via bash since it's a POSIX shell script Path bash = this.context.findBashRequired(); - this.context.newProcess() - .executable(bash) - .addArgs("-c", "cd \"" + goSrcDir + "\" && ./make.bash") - .run(ProcessMode.DEFAULT); + this.context.newProcess().executable(bash).directory(workingDir).addArgs("./make.bash").run(ProcessMode.DEFAULT); } else { - // On Linux/macOS, execute make.bash directly - this.context.newProcess() - .executable(makeScript) - .directory(goSrcDir) - .run(ProcessMode.DEFAULT); + this.context.newProcess().executable(makeBash).directory(workingDir).run(ProcessMode.DEFAULT); } - - LOG.debug("Go build completed successfully"); } } From 4a4f561141fbd94e8a87e32603f85b9ba19b17d2 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Wed, 18 Mar 2026 11:39:26 +0100 Subject: [PATCH 07/22] #1751: updated implementation of go installation, added tests and doc for go --- .../com/devonfw/tools/ide/tool/go/Go.java | 47 ++++- cli/src/main/resources/nls/Help.properties | 2 + cli/src/main/resources/nls/Help_de.properties | 2 + .../com/devonfw/tools/ide/tool/go/GoTest.java | 188 ++++++++++++++++++ .../go/_ide/urls/go/go/1.22.4/urls | 3 + .../go/project/settings/ide.properties | 0 .../go/project/workspaces/main/.gitkeep | 0 .../go/repository/go/go/default/src/make.bash | 3 + 8 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/tool/go/GoTest.java create mode 100644 cli/src/test/resources/ide-projects/go/_ide/urls/go/go/1.22.4/urls create mode 100644 cli/src/test/resources/ide-projects/go/project/settings/ide.properties create mode 100644 cli/src/test/resources/ide-projects/go/project/workspaces/main/.gitkeep create mode 100644 cli/src/test/resources/ide-projects/go/repository/go/go/default/src/make.bash diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java index 9ff272d7e..11499ada2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/go/Go.java @@ -44,23 +44,48 @@ protected void performToolInstallation(ToolInstallRequest request, Path installa } private void runGoBootstrapIfPresent(Path installationPath) { - Path makeBash = installationPath.resolve("make.bash"); - Path workingDir = installationPath; - if (!Files.isRegularFile(makeBash)) { - workingDir = installationPath.resolve("src"); - makeBash = workingDir.resolve("make.bash"); - } - if (!Files.isRegularFile(makeBash)) { - LOG.debug("No make.bash found in {} or {} - skipping source bootstrap.", installationPath, installationPath.resolve("src")); + Path makeScript = findGoBootstrapScript(installationPath); + if (makeScript == null) { + LOG.debug("No Go bootstrap script found in {} or {} - skipping source bootstrap.", installationPath, + installationPath.resolve("src")); return; } + runGoBootstrapScript(makeScript, makeScript.getParent()); + } + + private Path findGoBootstrapScript(Path installationPath) { + + Path script = findGoBootstrapScript(installationPath, "make.bash"); + if ((script == null) && this.context.getSystemInfo().isWindows()) { + script = findGoBootstrapScript(installationPath, "make.bat"); + } + return script; + } + + private Path findGoBootstrapScript(Path installationPath, String fileName) { + + Path rootScript = installationPath.resolve(fileName); + if (Files.isRegularFile(rootScript)) { + return rootScript; + } + Path srcScript = installationPath.resolve("src").resolve(fileName); + if (Files.isRegularFile(srcScript)) { + return srcScript; + } + return null; + } + + protected void runGoBootstrapScript(Path makeScript, Path workingDir) { - LOG.info("Running Go bootstrap script {}", makeBash); - if (this.context.getSystemInfo().isWindows()) { + LOG.info("Running Go bootstrap script {}", makeScript); + String scriptName = makeScript.getFileName().toString(); + if ("make.bat".equals(scriptName)) { + this.context.newProcess().executable(makeScript).directory(workingDir).run(ProcessMode.DEFAULT); + } else if (this.context.getSystemInfo().isWindows()) { Path bash = this.context.findBashRequired(); this.context.newProcess().executable(bash).directory(workingDir).addArgs("./make.bash").run(ProcessMode.DEFAULT); } else { - this.context.newProcess().executable(makeBash).directory(workingDir).run(ProcessMode.DEFAULT); + this.context.newProcess().executable(makeScript).directory(workingDir).run(ProcessMode.DEFAULT); } } diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index e4a10e7db..758709d61 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -35,6 +35,8 @@ cmd.get-version.opt.--configured=print only the configured version cmd.get-version.opt.--installed=print only the installed version cmd.gh=Tool commandlet for GitHub CLI. cmd.gh.detail=GitHub CLI (Command Line Interface) allows to interact with GitHub repositories, issues, and pull requests from the command line. Detailed documentation can be found at https://cli.github.com/manual/ +cmd.go=Tool commandlet for Go (programming language). +cmd.go.detail=Go is an open-source programming language for building simple, reliable, and efficient software. Detailed documentation can be found at https://go.dev/doc/ cmd.graalvm=Tool commandlet for GraalVm (Java with native-image). cmd.graalvm.detail=GraalVM is a high-performance runtime that supports multiple languages and execution modes. Detailed documentation can be found at https://www.graalvm.org/docs/ cmd.gradle=Tool commandlet for Gradle (Build-Tool). diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index cc45649ed..e9e8fb846 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -35,6 +35,8 @@ cmd.get-version.opt.--configured=zeigt nur die konfigurierte Version cmd.get-version.opt.--installed=zeigt nur die installierte Version cmd.gh=Werkzeug Kommando für die Github Kommandoschnittstelle. cmd.gh.detail=GitHub CLI (Command Line Interface) ermöglicht die Interaktion mit GitHub-Repositories, Issues und Pull Requests über die Befehlszeile. Detaillierte Dokumentation ist zu finden unter https://cli.github.com/manual/ +cmd.go=Werkzeug Kommando für Go (Programmiersprache). +cmd.go.detail=Go ist eine Open-Source-Programmiersprache zum Erstellen einfacher, zuverlässiger und effizienter Software. Detaillierte Dokumentation ist zu finden unter https://go.dev/doc/ cmd.graalvm=Werkzeug Kommando für GraalVm. cmd.graalvm.detail=GraalVM ist eine leistungsstarke Laufzeitumgebung, die mehrere Sprachen und Ausführungsmodi unterstützt. Detaillierte Dokumentation ist zu finden unter https://www.graalvm.org/docs/ cmd.gradle=Werkzeug Kommando für Gradle (Build-Tool). diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/go/GoTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/go/GoTest.java new file mode 100644 index 000000000..6cc92df2a --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/go/GoTest.java @@ -0,0 +1,188 @@ +package com.devonfw.tools.ide.tool.go; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.os.SystemInfoMock; +import com.devonfw.tools.ide.process.ProcessContext; +import com.devonfw.tools.ide.process.ProcessMode; +import com.devonfw.tools.ide.process.ProcessResult; +import com.devonfw.tools.ide.process.ProcessResultImpl; + +/** + * Test of {@link Go}. + */ +class GoTest extends AbstractIdeContextTest { + + private static final String PROJECT_GO = "go"; + + private static final String GO_VERSION = "1.22.4"; + + @ParameterizedTest + @ValueSource(strings = { "windows", "linux", "mac" }) + void testGoInstallInvokesBootstrapFromSrcForAllPlatforms(String os) { + + // arrange + IdeTestContext context = newContext(PROJECT_GO); + context.setSystemInfo(SystemInfoMock.of(os)); + GoSpy go = new GoSpy(context); + + // act + go.install(); + + // assert + assertThat(go.bootstrapCalled).isTrue(); + assertThat(go.bootstrapScript).isNotNull(); + assertThat(go.bootstrapScript.getFileName().toString()).isEqualTo("make.bash"); + assertThat(go.bootstrapWorkingDir).isNotNull(); + assertThat(go.bootstrapWorkingDir.getFileName().toString()).isEqualTo("src"); + assertThat(context.getSoftwarePath().resolve("go/.ide.software.version")).exists().hasContent(GO_VERSION); + assertThat(context).logAtSuccess().hasMessageContaining("Successfully installed go in version " + GO_VERSION); + } + + @Test + void testGoInstallSkipsBootstrapWhenScriptMissing() { + + // arrange + IdeTestContext context = newContext(PROJECT_GO); + context.getFileAccess().delete(context.getIdeRoot().resolve("repository/go/go/default/src/make.bash")); + GoSpy go = new GoSpy(context); + + // act + go.install(); + + // assert + assertThat(go.bootstrapCalled).isFalse(); + assertThat(context).logAtDebug().hasMessageContaining("No Go bootstrap script found"); + } + + @Test + void testGoInstallPrefersRootBootstrapScriptOverSrc() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_GO); + Path rootMakeBash = context.getIdeRoot().resolve("repository/go/go/default/make.bash"); + Files.writeString(rootMakeBash, "#!/usr/bin/env bash\nexit 0\n"); + GoSpy go = new GoSpy(context); + + // act + go.install(); + + // assert + assertThat(go.bootstrapCalled).isTrue(); + assertThat(go.bootstrapWorkingDir).isNotNull(); + assertThat(go.bootstrapWorkingDir.getFileName().toString()).isEqualTo(GO_VERSION); + assertThat(go.bootstrapScript).isEqualTo(go.bootstrapWorkingDir.resolve("make.bash")); + } + + @Test + void testGoInstallUsesMakeBatOnWindowsWhenNoMakeBash() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_GO); + context.setSystemInfo(SystemInfoMock.of("windows")); + context.getFileAccess().delete(context.getIdeRoot().resolve("repository/go/go/default/src/make.bash")); + Path makeBat = context.getIdeRoot().resolve("repository/go/go/default/src/make.bat"); + Files.writeString(makeBat, "@echo off\r\nexit /b 0\r\n"); + GoSpy go = new GoSpy(context); + + // act + go.install(); + + // assert + assertThat(go.bootstrapCalled).isTrue(); + assertThat(go.bootstrapScript).isNotNull(); + assertThat(go.bootstrapScript.getFileName().toString()).isEqualTo("make.bat"); + } + + @Test + void testRunGoBootstrapScriptUsesBashOnWindows() { + + // arrange + IdeTestContext context = mock(IdeTestContext.class); + ProcessContext process = mock(ProcessContext.class); + when(context.getSystemInfo()).thenReturn(SystemInfoMock.of("windows")); + when(context.findBashRequired()).thenReturn(Path.of("C:/tools/git/bin/bash.exe")); + when(context.newProcess()).thenReturn(process); + when(process.executable(Path.of("C:/tools/git/bin/bash.exe"))).thenReturn(process); + when(process.directory(Path.of("C:/go/src"))).thenReturn(process); + when(process.addArgs("./make.bash")).thenReturn(process); + when(process.run(ProcessMode.DEFAULT)).thenReturn(successResult()); + Go go = new Go(context); + + // act + go.runGoBootstrapScript(Path.of("C:/go/src/make.bash"), Path.of("C:/go/src")); + + // assert + verify(process).executable(Path.of("C:/tools/git/bin/bash.exe")); + verify(process).directory(Path.of("C:/go/src")); + verify(process).addArgs("./make.bash"); + verify(process).run(ProcessMode.DEFAULT); + } + + @ParameterizedTest + @ValueSource(strings = { "linux", "mac" }) + void testRunGoBootstrapScriptRunsScriptDirectlyOnUnix(String os) { + + // arrange + IdeTestContext context = mock(IdeTestContext.class); + ProcessContext process = mock(ProcessContext.class); + Path script = Path.of("/tmp/go/src/make.bash"); + Path workingDir = Path.of("/tmp/go/src"); + when(context.getSystemInfo()).thenReturn(SystemInfoMock.of(os)); + when(context.newProcess()).thenReturn(process); + when(process.executable(script)).thenReturn(process); + when(process.directory(workingDir)).thenReturn(process); + when(process.run(ProcessMode.DEFAULT)).thenReturn(successResult()); + Go go = new Go(context); + + // act + go.runGoBootstrapScript(script, workingDir); + + // assert + verify(process).executable(script); + verify(process).directory(workingDir); + verify(process, never()).addArg("./make.bash"); + verify(process).run(ProcessMode.DEFAULT); + } + + private ProcessResult successResult() { + + return new ProcessResultImpl("test", "test", ProcessResult.SUCCESS, true, Collections.emptyList()); + } + + private static class GoSpy extends Go { + + private boolean bootstrapCalled; + + private Path bootstrapScript; + + private Path bootstrapWorkingDir; + + private GoSpy(IdeTestContext context) { + + super(context); + } + + @Override + protected void runGoBootstrapScript(Path makeBash, Path workingDir) { + + this.bootstrapCalled = true; + this.bootstrapScript = makeBash; + this.bootstrapWorkingDir = workingDir; + } + } +} diff --git a/cli/src/test/resources/ide-projects/go/_ide/urls/go/go/1.22.4/urls b/cli/src/test/resources/ide-projects/go/_ide/urls/go/go/1.22.4/urls new file mode 100644 index 000000000..cf238aa5e --- /dev/null +++ b/cli/src/test/resources/ide-projects/go/_ide/urls/go/go/1.22.4/urls @@ -0,0 +1,3 @@ +UNKNOWN + +https://example.org/go-1.22.4.tar.gz diff --git a/cli/src/test/resources/ide-projects/go/project/settings/ide.properties b/cli/src/test/resources/ide-projects/go/project/settings/ide.properties new file mode 100644 index 000000000..e69de29bb diff --git a/cli/src/test/resources/ide-projects/go/project/workspaces/main/.gitkeep b/cli/src/test/resources/ide-projects/go/project/workspaces/main/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cli/src/test/resources/ide-projects/go/repository/go/go/default/src/make.bash b/cli/src/test/resources/ide-projects/go/repository/go/go/default/src/make.bash new file mode 100644 index 000000000..63181239d --- /dev/null +++ b/cli/src/test/resources/ide-projects/go/repository/go/go/default/src/make.bash @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "bootstrap placeholder" + From e20a599d540cc0e2fdfe3cec3c14a96808198af1 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Wed, 18 Mar 2026 14:06:54 +0100 Subject: [PATCH 08/22] #1751: added go-lang support for cli to changelog --- CHANGELOG.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c9d1a223d..6ad78a0aa 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,8 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1751[#1751]: Add go-lang CLI Commandlet. + The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/42?closed=1[milestone 2026.04.001]. From 10f98808a40ea5af8384d36e010492d737e2ca89 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Tue, 24 Mar 2026 12:35:41 +0100 Subject: [PATCH 09/22] #1687: added argument to suppress native warning --- cli/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/pom.xml b/cli/pom.xml index 34df7f305..9fe7cd2bb 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -243,6 +243,7 @@ ${imageName} + --enable-native-access=ALL-UNNAMED --enable-url-protocols=http,https -march=compatibility --initialize-at-build-time=org.apache.commons From 189ed5735ee8dbe8689f65778249c14f1f3b8b8b Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Thu, 26 Mar 2026 12:19:40 +0100 Subject: [PATCH 10/22] #1687: updated changelog --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index fd27842df..e083b7647 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -12,6 +12,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1747[#1747]: Fixed macOS x64 native image build using macos-15-intel runner * https://github.com/devonfw/IDEasy/issues/1738[#1738]: FileAccess.delete no longer follows directory links during recursive delete * https://github.com/devonfw/IDEasy/issues/1771[#1771]: Maven version 3.9.1x are now available +* https://github.com/devonfw/IDEasy/issues/1687[#1687]: Fixed JLine warning about restricted method The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/42?closed=1[milestone 2026.04.001]. From 5779788df8693eb58ab5e28ef987ea1b868c98e5 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Fri, 27 Mar 2026 14:04:20 +0100 Subject: [PATCH 11/22] #1552: integrated a truststore commandlet to create a custom truststore --- .../devonfw/tools/ide/commandlet/TruststoreCommandlet.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java new file mode 100644 index 000000000..e12c6c3e2 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java @@ -0,0 +1,4 @@ +package com.devonfw.tools.ide.commandlet; + +public class TruststoreCommandlet { +} From a4d5fdc6a8417396a2ebd251daafcaf8b5366812 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Fri, 27 Mar 2026 14:04:38 +0100 Subject: [PATCH 12/22] #1552: integrated a truststore commandlet to create a custom truststore --- .../ide/commandlet/CommandletManagerImpl.java | 1 + .../ide/commandlet/TruststoreCommandlet.java | 194 +++++++- .../ide/truststore/TruststoreUtilImpl.java | 441 ++++++++++++++++++ 3 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index c44f5b740..4946f6263 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -109,6 +109,7 @@ public CommandletManagerImpl(IdeContext context) { add(new InstallPluginCommandlet(context)); add(new UninstallPluginCommandlet(context)); add(new UpgradeCommandlet(context)); + add(new TruststoreCommandlet(context)); add(new Gh(context)); add(new Helm(context)); add(new Java(context)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java index e12c6c3e2..03806d7d6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java @@ -1,4 +1,196 @@ package com.devonfw.tools.ide.commandlet; -public class TruststoreCommandlet { +import java.nio.file.Path; +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.property.StringProperty; +import com.devonfw.tools.ide.truststore.TruststoreUtilImpl; + +/** + * {@link Commandlet} to fix the TLS problem for VPN users. + */ +public class TruststoreCommandlet extends Commandlet { + + private static final Logger LOG = LoggerFactory.getLogger(TruststoreCommandlet.class); + + private static final String IDE_OPTIONS = "IDE_OPTIONS"; + + private static final String TRUSTSTORE_OPTION_PREFIX = "-Djavax.net.ssl.trustStore="; + + private static final String TRUSTSTORE_PASSWORD_OPTION_PREFIX = "-Djavax.net.ssl.trustStorePassword="; + + private final StringProperty url; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public TruststoreCommandlet(IdeContext context) { + super(context); + addKeyword(getName()); + this.url = add(new StringProperty("", true, "url")); + } + + @Override + public String getName() { + + return "fix-vpn-tls-problem"; + } + + // Steps: + // 1. Check if there was an TLS issue before, if not, log and return. + // - Check if there was an SSLHandshakeException with a message like "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" + // 2. Fetch the URL again and retrieve Certificate Chain and save it + // 3. Print the untrusted Certificate including extensions + // 4. Ask the user if they want to add the certificate to a custom truststore + // 5. If yes, create or update the custom truststore + // - Create + // - Create new Truststore file in IDEasy settings (e.g. settings/truststore/truststore.p12) + // - Retrieve certificates from default truststore (e.g. cacerts) and add them to the new truststore + // - Add the new certificate to the new truststore + // - Create new Alias for the new certificate (e.g. "custom-1", "custom-2", etc.) + // - Update + // - Load existing custom truststore + // - Check if the certificate is already in the truststore, if yes, log and return + // - Add the new certificate to the new truststore + // - Create new Alias for the new certificate (e.g. "custom-1", "custom-2", etc.) + // 6. Fetch the URL again with the new truststore and check if the TLS issue is resolved, + // - if yes, log success, + // - if not, log failure and potential next steps (e.g. check if you are behind a corporate proxy and need to configure it in IDEasy). + @Override + protected void doRun() { + + String endpointInput = this.url.getValueAsString(); + TruststoreUtilImpl.TlsEndpoint endpoint; + try { + endpoint = TruststoreUtilImpl.parseTlsEndpoint(endpointInput); + } catch (IllegalArgumentException e) { + throw new CliException("Invalid target URL/host '" + endpointInput + "': " + e.getMessage(), e); + } + + String host = endpoint.host(); + int port = endpoint.port(); + Path customTruststorePath = this.context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); + + if (TruststoreUtilImpl.isTruststorePresent(customTruststorePath) + && TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { + IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath); + configureIdeOptions(customTruststorePath); + return; + } + + if (TruststoreUtilImpl.isReachable(host, port)) { + LOG.info("Successfully connected to {}:{} without certificate changes.", host, port); + LOG.info("No truststore update is required for the given address."); + return; + } + + LOG.info("The given address {}:{} is not reachable/valid without certificate changes. Continuing with certificate capture.", host, port); + + X509Certificate certificate; + try { + certificate = TruststoreUtilImpl.fetchServerCertificate(host, port); + } catch (Exception e) { + LOG.error("Failed to capture certificate from {}:{}.", host, 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(TruststoreUtilImpl.describeCertificate(certificate)); + + boolean addToTruststore = this.context.question("Do you want to add this certificate to the custom truststore at {}?", customTruststorePath); + + if (!addToTruststore) { + LOG.info("Skipped truststore update by user choice."); + return; + } + + try { + TruststoreUtilImpl.createOrUpdateTruststore(customTruststorePath, certificate, "custom"); + IdeLogLevel.SUCCESS.log(LOG, "Custom truststore updated at {}", customTruststorePath); + } catch (Exception e) { + LOG.error("Failed to create or update custom truststore at {}", customTruststorePath, e); + return; + } + + configureIdeOptions(customTruststorePath); + + if (TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { + IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with custom truststore."); + } else { + LOG.warn("TLS handshake still fails even with custom truststore."); + } + } + + private void configureIdeOptions(Path customTruststorePath) { + String truststorePath = customTruststorePath.toAbsolutePath().toString(); + String truststoreOption = TRUSTSTORE_OPTION_PREFIX + truststorePath; + String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + "changeit"; + + EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.CONF); + if (confVariables == null) { + IdeLogLevel.INTERACTION.log(LOG, + "Please configure IDE_OPTIONS manually: {} {}", + truststoreOption, + truststorePasswordOption); + return; + } + + String options = confVariables.getFlat(IDE_OPTIONS); + options = removeOptionWithPrefix(options, TRUSTSTORE_OPTION_PREFIX); + options = removeOptionWithPrefix(options, TRUSTSTORE_PASSWORD_OPTION_PREFIX); + options = appendOption(options, truststoreOption); + options = appendOption(options, truststorePasswordOption); + + try { + confVariables.set(IDE_OPTIONS, options, true); + confVariables.save(); + // Apply directly for the current process as well. + System.setProperty("javax.net.ssl.trustStore", truststorePath); + System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + IdeLogLevel.SUCCESS.log(LOG, "IDE_OPTIONS configured to use custom truststore by default."); + } catch (UnsupportedOperationException e) { + IdeLogLevel.INTERACTION.log(LOG, + "Please configure IDE_OPTIONS manually: {} {}", + truststoreOption, + truststorePasswordOption); + } + } + + private static String removeOptionWithPrefix(String options, String prefix) { + if ((options == null) || options.isBlank()) { + return ""; + } + StringBuilder result = new StringBuilder(); + String[] tokens = options.trim().split("\\s+"); + for (String token : tokens) { + if (!token.startsWith(prefix)) { + if (!result.isEmpty()) { + result.append(' '); + } + result.append(token); + } + } + return result.toString(); + } + + private static String appendOption(String options, String option) { + if ((options == null) || options.isBlank()) { + return option; + } + return options + " " + option; + } + + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java b/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java new file mode 100644 index 000000000..3d1cbaaca --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java @@ -0,0 +1,441 @@ +package com.devonfw.tools.ide.truststore; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * Utility methods for truststore handling and TLS certificate capture. + */ +public final class TruststoreUtilImpl { + + /** + * Parsed TLS endpoint with host and port. + * + * @param host the server host. + * @param port the server port. + */ + public record TlsEndpoint(String host, int port) { + + } + + public static final char[] DEFAULT_CACERTS_PASSWORD = "changeit".toCharArray(); + + public static final char[] CUSTOM_TRUSTSTORE_PASSWORD = "changeit".toCharArray(); + + public static final String DEFAULT_ALIAS_PREFIX = "custom"; + + private static final int DEFAULT_TIMEOUT_MILLIS = 10_000; + + private TruststoreUtilImpl() { + // utility class + } + + /** + * Checks if a truststore file exists at the specified path. + * + * @param path the path to the truststore file. + * @return {@code true} if a truststore file exists at the specified path, {@code false} otherwise. + */ + public static boolean isTruststorePresent(Path path) { + return (path != null) && Files.exists(path); + } + + /** + * Loads the default Java truststore from the JRE cacerts file. + * + * @return the default Java truststore loaded from the JRE cacerts file. + * @throws Exception if an error occurs while loading the default truststore. + */ + public static KeyStore getDefaultTruststore() throws Exception { + String javaHome = System.getProperty("java.home"); + Path cacertsPath = Path.of(javaHome, "lib", "security", "cacerts"); + if (!Files.exists(cacertsPath)) { + throw new IllegalStateException("Default cacerts not found: " + cacertsPath); + } + + KeyStore cacerts = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream in = Files.newInputStream(cacertsPath)) { + cacerts.load(in, DEFAULT_CACERTS_PASSWORD); + } + return cacerts; + } + + /** + * Copies all certificate entries from the source truststore to the target truststore. Key entries are not copied, but if a key entry is encountered, its + * first certificate in the chain is copied as a certificate entry. + * + * @param source the source truststore to copy from. + * @param target the target truststore to copy to. + * @throws Exception if an error occurs while copying the truststore. + */ + public static void copyTruststore(KeyStore source, KeyStore target) throws Exception { + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(target, "target"); + + Enumeration aliases = source.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (source.isCertificateEntry(alias)) { + Certificate cert = source.getCertificate(alias); + if (cert != null) { + target.setCertificateEntry(alias, cert); + } + } else if (source.isKeyEntry(alias)) { + Certificate[] chain = source.getCertificateChain(alias); + if ((chain != null) && (chain.length > 0)) { + target.setCertificateEntry(alias, chain[0]); + } + } + } + } + + /** + * Creates a new truststore at the specified path or updates an existing one by adding the given certificate if it is not already present. If the truststore + * does not + * + * @param customTruststorePath the path to the custom truststore file to create or update. + * @param certificate the certificate to add to the truststore if not already present. + * @param aliasPrefix the prefix to use for the alias of the new certificate (e.g. "custom"). If {@code null} or blank, a default prefix is used. + * @throws Exception if an error occurs while creating or updating the truststore. + */ + public static void createOrUpdateTruststore(Path customTruststorePath, X509Certificate certificate, String aliasPrefix) throws Exception { + Objects.requireNonNull(customTruststorePath, "customTruststorePath"); + Objects.requireNonNull(certificate, "certificate"); + + if ((aliasPrefix == null) || aliasPrefix.isBlank()) { + aliasPrefix = DEFAULT_ALIAS_PREFIX; + } + + Path parent = customTruststorePath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + + KeyStore customStore = KeyStore.getInstance("PKCS12"); + if (isTruststorePresent(customTruststorePath)) { + try (InputStream in = Files.newInputStream(customTruststorePath)) { + customStore.load(in, CUSTOM_TRUSTSTORE_PASSWORD); + } + } else { + customStore.load(null, CUSTOM_TRUSTSTORE_PASSWORD); + copyTruststore(getDefaultTruststore(), customStore); + } + + if (!containsCertificate(customStore, certificate)) { + String alias = makeUniqueAlias(customStore, aliasPrefix); + addCertificate(customStore, alias, certificate); + } + + try (OutputStream out = Files.newOutputStream(customTruststorePath)) { + customStore.store(out, CUSTOM_TRUSTSTORE_PASSWORD); + } + } + + /** + * Adds the given certificate to the truststore under the specified alias. If the alias already exists, it will be overwritten. + * + * @param truststore the truststore to add the certificate to. + * @param alias the alias under which to add the certificate. + * @param certificate the certificate to add to the truststore. + * @throws Exception if an error occurs while adding the certificate to the truststore. + */ + public static void addCertificate(KeyStore truststore, String alias, X509Certificate certificate) throws Exception { + Objects.requireNonNull(truststore, "truststore"); + Objects.requireNonNull(alias, "alias"); + Objects.requireNonNull(certificate, "certificate"); + truststore.setCertificateEntry(alias, certificate); + } + + /** + * Parses a user input to a TLS endpoint supporting forms like {@code host}, {@code host:port}, and {@code https://host[:port]/path}. + * + * @param input the user input. + * @return the parsed {@link TlsEndpoint}. + */ + public static TlsEndpoint parseTlsEndpoint(String input) { + if ((input == null) || input.isBlank()) { + throw new IllegalArgumentException("URL/host must not be empty."); + } + String candidate = input.trim(); + + if (candidate.startsWith("http://")) { + throw new IllegalArgumentException("Only HTTPS URLs are supported: " + input); + } + + if (candidate.startsWith("https://")) { + return parseEndpointFromUri(input, URI.create(candidate)); + } + + if (candidate.contains("://")) { + URI uri = URI.create(candidate); + String scheme = uri.getScheme(); + if ((scheme == null) || !"https".equals(scheme.toLowerCase(Locale.ROOT))) { + throw new IllegalArgumentException("Only HTTPS URLs are supported: " + input); + } + return parseEndpointFromUri(input, uri); + } + + int separatorIndex = candidate.lastIndexOf(':'); + if (separatorIndex > 0 && separatorIndex < (candidate.length() - 1) && candidate.indexOf(':') == separatorIndex) { + String host = candidate.substring(0, separatorIndex).trim(); + String portPart = candidate.substring(separatorIndex + 1).trim(); + int port; + try { + port = Integer.parseInt(portPart); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port in input: " + input, e); + } + validateEndpoint(host, port, input); + return new TlsEndpoint(host, port); + } + + validateEndpoint(candidate, 443, input); + return new TlsEndpoint(candidate, 443); + } + + private static TlsEndpoint parseEndpointFromUri(String input, URI uri) { + String host = uri.getHost(); + int port = (uri.getPort() > 0) ? uri.getPort() : 443; + validateEndpoint(host, port, input); + return new TlsEndpoint(host, port); + } + + private static void validateEndpoint(String host, int port, String input) { + if ((host == null) || host.isBlank()) { + throw new IllegalArgumentException("Missing host in input: " + input); + } + if ((port < 1) || (port > 65535)) { + throw new IllegalArgumentException("Port out of range in input: " + input); + } + } + + /** + * Checks if a TLS endpoint can be reached and validated with the current default trust configuration. + * + * @param host the server host to connect to. + * @param port the server port to connect to. + * @return {@code true} if TLS handshake succeeds without truststore changes, {@code false} otherwise. + */ + public static boolean isReachable(String host, int port) { + validateEndpoint(host, port, host + ":" + port); + SSLSocket socket = null; + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, new SecureRandom()); + SSLSocketFactory factory = sslContext.getSocketFactory(); + socket = connectTlsSocket(factory, host, port); + socket.startHandshake(); + return true; + } catch (Exception e) { + return false; + } finally { + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + // ignore close failure after reachability probe + } + } + } + } + + /** + * Checks if a TLS endpoint can be reached and validated using the provided custom truststore. + * + * @param host the server host to connect to. + * @param port the server port to connect to. + * @param truststorePath the path to the custom truststore to use. + * @return {@code true} if TLS handshake succeeds with the custom truststore, {@code false} otherwise. + */ + public static boolean isReachable(String host, int port, Path truststorePath) { + validateEndpoint(host, port, host + ":" + port); + Objects.requireNonNull(truststorePath, "truststorePath"); + try { + verifyConnectionWithTruststore(host, port, truststorePath); + return true; + } catch (Exception e) { + return false; + } + } + + private static SSLSocket connectTlsSocket(SSLSocketFactory factory, String host, int port) throws Exception { + SSLSocket socket = (SSLSocket) factory.createSocket(); + try { + socket.connect(new InetSocketAddress(host, port), DEFAULT_TIMEOUT_MILLIS); + socket.setSoTimeout(DEFAULT_TIMEOUT_MILLIS); + return socket; + } catch (Exception e) { + try { + socket.close(); + } catch (Exception ignored) { + // ignore close failures on unsuccessful connect + } + throw e; + } + } + + /** + * Fetches the server certificate from the specified host and port by performing a TLS handshake and capturing the certificate chain using a custom trust + * manager. + * + * @param host the server host to connect to. + * @param port the server port to connect to. + * @return the server certificate captured from the TLS handshake. + * @throws Exception if an error occurs while fetching the server certificate, e.g. due to connection issues or if the server does not provide a + * certificate chain. + */ + public static X509Certificate fetchServerCertificate(String host, int port) throws Exception { + Objects.requireNonNull(host, "host"); + if (host.isBlank()) { + throw new IllegalArgumentException("host must not be blank"); + } + if ((port < 1) || (port > 65535)) { + throw new IllegalArgumentException("port must be between 1 and 65535"); + } + + SavingTrustManager savingTrustManager = new SavingTrustManager(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { savingTrustManager }, new SecureRandom()); + + SSLSocketFactory factory = sslContext.getSocketFactory(); + try (SSLSocket socket = connectTlsSocket(factory, host, port)) { + try { + socket.startHandshake(); + } catch (SSLException e) { + // expected: trust manager aborts after capturing the chain + } + } + + X509Certificate[] chain = savingTrustManager.getChain(); + if ((chain == null) || (chain.length == 0)) { + throw new CertificateException("Could not capture server certificate chain from " + host + ":" + port); + } + + return chain[chain.length - 1]; + } + + /** + * Verifies that a TLS connection to the specified host and port can be established using the truststore at the given path by performing a TLS handshake. If + * the handshake is successful, the method returns normally. If the handshake fails due to trust issues, an SSLException is thrown. + * + * @param host the server host to connect to. + * @param port the server port to connect to. + * @param truststorePath the path to the truststore file to use for the TLS handshake. + * @throws Exception if an error occurs while verifying the connection, e.g. due to connection issues, TLS handshake failure, or if the truststore file + * cannot be loaded. + */ + public static void verifyConnectionWithTruststore(String host, int port, Path truststorePath) throws Exception { + KeyStore truststore = KeyStore.getInstance("PKCS12"); + try (InputStream in = Files.newInputStream(truststorePath)) { + truststore.load(in, CUSTOM_TRUSTSTORE_PASSWORD); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(truststore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); + + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + try (SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port)) { + socket.setSoTimeout(DEFAULT_TIMEOUT_MILLIS); + socket.startHandshake(); + } + } + + + /** + * Generates a human-readable description of the given X.509 certificate including subject, issuer, serial number, validity period, signature algorithm, and + * + * @param certificate the certificate to describe. + * @return a human-readable description of the given X.509 certificate. + */ + public static String describeCertificate(X509Certificate certificate) { + String nl = System.lineSeparator(); + StringBuilder sb = new StringBuilder(); + sb.append("Subject: ").append(certificate.getSubjectX500Principal()).append(nl); + sb.append("Issuer : ").append(certificate.getIssuerX500Principal()).append(nl); + sb.append("Serial : ").append(certificate.getSerialNumber()).append(nl); + sb.append("Valid : ").append(certificate.getNotBefore()).append(" -> ").append(certificate.getNotAfter()).append(nl); + sb.append("SigAlg : ").append(certificate.getSigAlgName()).append(nl); + + Set critical = certificate.getCriticalExtensionOIDs(); + Set nonCritical = certificate.getNonCriticalExtensionOIDs(); + sb.append("Critical extensions : ").append((critical == null) ? "[]" : critical).append(nl); + sb.append("Non-critical extensions: ").append((nonCritical == null) ? "[]" : nonCritical); + + return sb.toString(); + } + + private static boolean containsCertificate(KeyStore keyStore, X509Certificate certificate) throws Exception { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + Certificate existing = keyStore.getCertificate(alias); + if ((existing instanceof X509Certificate existingX509) && Arrays.equals(existingX509.getEncoded(), certificate.getEncoded())) { + return true; + } + } + return false; + } + + private static String makeUniqueAlias(KeyStore keyStore, String baseAlias) throws Exception { + String alias = baseAlias; + int i = 1; + while (keyStore.containsAlias(alias)) { + alias = baseAlias + "-" + i; + i++; + } + return alias; + } + + private static final class SavingTrustManager implements X509TrustManager { + + private X509Certificate[] chain; + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // not needed + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + this.chain = (chain == null) ? null : Arrays.copyOf(chain, chain.length); + if ((chain == null) || (chain.length == 0)) { + throw new CertificateException("Server certificate chain is empty"); + } + throw new CertificateException("Captured server certificate chain"); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public X509Certificate[] getChain() { + return this.chain; + } + } +} From 3736c9ca3d0a8d668a1e4d9c966a429c2f7b00f5 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 30 Mar 2026 11:46:52 +0100 Subject: [PATCH 13/22] #1552: added tests, description for the commandlet and remove changable password for the custom truststore --- .../ide/commandlet/TruststoreCommandlet.java | 63 +++++++++---------- cli/src/main/resources/nls/Help.properties | 3 + cli/src/main/resources/nls/Help_de.properties | 3 + 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java index 03806d7d6..eeebb2af7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java @@ -11,6 +11,7 @@ import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.nls.NlsBundle; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.truststore.TruststoreUtilImpl; @@ -27,6 +28,8 @@ public class TruststoreCommandlet extends Commandlet { private static final String TRUSTSTORE_PASSWORD_OPTION_PREFIX = "-Djavax.net.ssl.trustStorePassword="; + private static final String DEFAULT_TRUSTSTORE_PASSWORD = "changeit"; + private final StringProperty url; /** @@ -46,26 +49,23 @@ public String getName() { return "fix-vpn-tls-problem"; } - // Steps: - // 1. Check if there was an TLS issue before, if not, log and return. - // - Check if there was an SSLHandshakeException with a message like "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target" - // 2. Fetch the URL again and retrieve Certificate Chain and save it - // 3. Print the untrusted Certificate including extensions - // 4. Ask the user if they want to add the certificate to a custom truststore - // 5. If yes, create or update the custom truststore - // - Create - // - Create new Truststore file in IDEasy settings (e.g. settings/truststore/truststore.p12) - // - Retrieve certificates from default truststore (e.g. cacerts) and add them to the new truststore - // - Add the new certificate to the new truststore - // - Create new Alias for the new certificate (e.g. "custom-1", "custom-2", etc.) - // - Update - // - Load existing custom truststore - // - Check if the certificate is already in the truststore, if yes, log and return - // - Add the new certificate to the new truststore - // - Create new Alias for the new certificate (e.g. "custom-1", "custom-2", etc.) - // 6. Fetch the URL again with the new truststore and check if the TLS issue is resolved, - // - if yes, log success, - // - if not, log failure and potential next steps (e.g. check if you are behind a corporate proxy and need to configure it in IDEasy). + /** + * This commandlet tries to fix TLS problems for VPN users by capturing the untrusted certificate from the target endpoint and adding it to a custom + * truststore. It also configures IDE_OPTIONS to use the custom truststore by default. The commandlet is idempotent and will not make changes if the endpoint + * is already reachable or if the certificate is already trusted. + *

+ * The flow is as follows: + *

    + *
  • Parse the input URL/host and port.
  • + *
  • Check if a custom truststore already exists and can establish a TLS connection to the endpoint. If yes, exit successfully.
  • + *
  • Check if the endpoint is reachable without any certificate changes. If yes, exit successfully.
  • + *
  • Try to capture the server certificate from the endpoint. If it fails, log an error and exit.
  • + *
  • Show the captured certificate details to the user and ask if they want to add it to the custom truststore.
  • + *
  • If the user agrees, ask for a password for the custom truststore and create/update it with the captured certificate.
  • + *
  • Configure IDE_OPTIONS to use the custom truststore by default.
  • + *
  • Check if the endpoint is now reachable with the custom truststore and log the result.
  • + *
+ */ @Override protected void doRun() { @@ -81,8 +81,7 @@ protected void doRun() { int port = endpoint.port(); Path customTruststorePath = this.context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); - if (TruststoreUtilImpl.isTruststorePresent(customTruststorePath) - && TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { + if (TruststoreUtilImpl.isTruststorePresent(customTruststorePath) && TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath); configureIdeOptions(customTruststorePath); return; @@ -136,14 +135,11 @@ protected void doRun() { private void configureIdeOptions(Path customTruststorePath) { String truststorePath = customTruststorePath.toAbsolutePath().toString(); String truststoreOption = TRUSTSTORE_OPTION_PREFIX + truststorePath; - String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + "changeit"; + String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + DEFAULT_TRUSTSTORE_PASSWORD; EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.CONF); if (confVariables == null) { - IdeLogLevel.INTERACTION.log(LOG, - "Please configure IDE_OPTIONS manually: {} {}", - truststoreOption, - truststorePasswordOption); + IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption); return; } @@ -158,13 +154,10 @@ private void configureIdeOptions(Path customTruststorePath) { confVariables.save(); // Apply directly for the current process as well. System.setProperty("javax.net.ssl.trustStore", truststorePath); - System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + System.setProperty("javax.net.ssl.trustStorePassword", DEFAULT_TRUSTSTORE_PASSWORD); IdeLogLevel.SUCCESS.log(LOG, "IDE_OPTIONS configured to use custom truststore by default."); } catch (UnsupportedOperationException e) { - IdeLogLevel.INTERACTION.log(LOG, - "Please configure IDE_OPTIONS manually: {} {}", - truststoreOption, - truststorePasswordOption); + IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption); } } @@ -192,5 +185,9 @@ private static String appendOption(String options, String option) { return options + " " + option; } - + @Override + public void printHelp(NlsBundle bundle) { + LOG.info( + "This commandlet helps to fix TLS issues for users behind VPNs by capturing untrusted certificates from target endpoints and adding them to a custom truststore. It also configures IDE_OPTIONS to use the custom truststore by default. The commandlet is idempotent and will not make changes if the endpoint is already reachable or if the certificate is already trusted."); + } } diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 758709d61..cbb454880 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -23,6 +23,9 @@ cmd.eclipse.detail=Eclipse IDE is an open-source Integrated Development Environm cmd.env=Prints the environment variables to set and export. cmd.env.detail=To print all active environment variables of IDEasy simply type: 'ide env'. If you add the '--debug' flag to ide e.g. 'ide --debug env' IDEasy will also add additional information about the locations of the definitions to the output. cmd.env.opt.--bash=Convert Windows path syntax to bash for usage in git-bash. +cmd.fix-vpn-tls-problem=Commandlet to fix the VPN TLS problem on Windows. +cmd.fix-vpn-tls-problem.detail=If you are using a VPN on Windows and encounter TLS problems, this commandlet can help you fix the issue by adding the necessary certificates to your Java keystore. Simply run 'ide fix-vpn-tls-problem' and follow the instructions provided in the console output. +cmd.fix-vpn-tls-problem.val.url=The URL that is affected by the VPN TLS problem (e.g. 'https://api.azul.com'). cmd.gcviewer=Tool commandlet for GC Viewer (View garbage collector logs of Java). cmd.gcviewer.detail=GCViewer is a tool for analyzing and visualizing Java garbage collection logs. Detailed documentation can be found at https://github.com/chewiebug/GCViewer cmd.get-edition=Get the edition of the selected tool. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index e9e8fb846..d72d04a64 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -23,6 +23,9 @@ cmd.eclipse.detail=Eclipse ist eine Open-Source-Entwicklungsumgebung für die En cmd.env=Gibt die zu setzenden und exportierenden Umgebungsvariablen aus. cmd.env.detail=Um alle aktiven Umgebungsvariablen von IDEasy auszugeben, geben Sie einfach 'ide env' in die Konsole ein. Um zusätzlich noch die Ursprünge der Variablen ausgegeben zu bekommen, fügen Sie einfach das debug flag '--debug' hinzu z.B. 'ide --debug env'. cmd.env.opt.--bash=Konvertiert Windows-Pfad-Syntax nach Bash zur Verwendung in git-bash. +cmd.fix-vpn-tls-problem=Wekzeug Kommando zum Beheben von VPN TLS Problemen auf Windows +cmd.fix-vpn-tls-problem.detail=Auf einigen Windows-Systemen kann es zu Problemen mit der TLS-Verbindung kommen, wenn eine VPN-Verbindung aktiv ist. Dieses Problem kann dazu führen, dass bestimmte Werkzeuge oder Dienste nicht ordnungsgemäß funktionieren. Das Kommando 'ide fix-vpn-tls-problem' bietet eine Lösung für dieses Problem, indem es die TLS-Einstellungen anpasst, um die Kompatibilität mit VPN-Verbindungen zu verbessern. Um dieses Problem zu beheben, führen Sie einfach den folgenden Befehl aus: 'ide fix-vpn-tls-problem --url '. Ersetzen Sie durch die URL, die von dem VPN TLS Problem betroffen ist (z.B. 'https://api.azul.com'). +cmd.fix-vpn-tls-problem.val.url=Die URL, die von dem VPN TLS Problem betroffen ist (z.B. 'https://api.azul.com'). cmd.gcviewer=Werkzeug Kommando für GC Viewer (Anzeige von Garbage-Collector Logs von Java). cmd.gcviewer.detail=GCViewer ist ein Tool zur Analyse und Visualisierung von Java-Garbage-Collection-Protokollen. Detaillierte Dokumentation ist zu finden unter https://github.com/chewiebug/GCViewer cmd.get-edition=Zeigt die Edition des selektierten Werkzeugs an. From 61b29a88c120fdd1124b0141db885c93f90172ba Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 30 Mar 2026 11:48:30 +0100 Subject: [PATCH 14/22] #1552: added tests for truststore cmdlet --- .../commandlet/TruststoreCommandletTest.java | 135 ++++++++++++ .../truststore/TruststoreUtilImplTest.java | 195 ++++++++++++++++++ .../test/resources/truststore/test-cert.pem | 22 ++ 3 files changed, 352 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java create mode 100644 cli/src/test/resources/truststore/test-cert.pem diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java new file mode 100644 index 000000000..111063774 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java @@ -0,0 +1,135 @@ +package com.devonfw.tools.ide.commandlet; + +import java.lang.reflect.Method; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.property.Property; + +/** + * Test of {@link TruststoreCommandlet}. + */ +class TruststoreCommandletTest extends AbstractIdeContextTest { + + private static final String IDE_OPTIONS = "IDE_OPTIONS"; + + private static final String TRUSTSTORE_PASSWORD = "changeit"; + + private String previousTruststore; + + private String previousTruststorePassword; + + @AfterEach + void cleanSystemProperties() { + + restoreSystemProperty("javax.net.ssl.trustStore", this.previousTruststore); + restoreSystemProperty("javax.net.ssl.trustStorePassword", this.previousTruststorePassword); + } + + @Test + void testRunWithInvalidEndpointThrowsCliException(@TempDir Path tempDir) { + + IdeTestContext context = newIsolatedContext(tempDir); + TruststoreCommandlet commandlet = context.getCommandletManager().getCommandlet(TruststoreCommandlet.class); + setUrl(commandlet, context, "http://github.com"); + + assertThatThrownBy(commandlet::run).isInstanceOf(CliException.class) + .hasMessageContaining("Invalid target URL/host") + .hasMessageContaining("Only HTTPS URLs are supported"); + } + + @Test + void testRunWithUnreachableEndpointLogsHintAndKeepsTruststoreUntouched(@TempDir Path tempDir) { + + IdeTestContext context = newIsolatedContext(tempDir); + TruststoreCommandlet commandlet = context.getCommandletManager().getCommandlet(TruststoreCommandlet.class); + setUrl(commandlet, context, "https://127.0.0.1:9"); + + Path customTruststorePath = context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); + commandlet.run(); + + assertThat(customTruststorePath).doesNotExist(); + assertThat(context).logAtInfo().hasMessageContaining("is not reachable/valid without certificate changes"); + assertThat(context).logAtInteraction().hasMessageContaining("proxy-support.adoc#tls-certificate-issues"); + } + + @Test + void testConfigureIdeOptionsReplacesExistingTruststoreOptions(@TempDir Path tempDir) throws Exception { + + IdeTestContext context = newIsolatedContext(tempDir); + TruststoreCommandlet commandlet = context.getCommandletManager().getCommandlet(TruststoreCommandlet.class); + rememberSystemProperties(); + + EnvironmentVariables confVariables = context.getVariables().getByType(EnvironmentVariablesType.CONF); + confVariables.set(IDE_OPTIONS, + "-Xmx512m -Djavax.net.ssl.trustStore=/old/path.p12 -Djavax.net.ssl.trustStorePassword=old-secret"); + + Path newTruststorePath = context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); + + invokeConfigureIdeOptions(commandlet, newTruststorePath); + + String options = confVariables.getFlat(IDE_OPTIONS); + assertThat(options).contains("-Xmx512m"); + assertThat(options).contains("-Djavax.net.ssl.trustStore=" + newTruststorePath.toAbsolutePath()); + assertThat(options).contains("-Djavax.net.ssl.trustStorePassword=" + TRUSTSTORE_PASSWORD); + assertThat(options).doesNotContain("/old/path.p12"); + assertThat(options).doesNotContain("old-secret"); + + assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo(newTruststorePath.toAbsolutePath().toString()); + assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo(TRUSTSTORE_PASSWORD); + } + + private IdeTestContext newIsolatedContext(Path tempDir) { + + IdeTestContext context = newContext(PROJECT_BASIC); + Path isolatedSettingsPath = tempDir.resolve("settings"); + assertThat(isolatedSettingsPath.startsWith(tempDir)).isTrue(); + context.setSettingsPath(isolatedSettingsPath); + assertThat(context.getSettingsPath()).isEqualTo(isolatedSettingsPath); + return context; + } + + private static void setUrl(TruststoreCommandlet commandlet, IdeTestContext context, String url) { + + Property endpointValue = commandlet.getValues().get(1); + endpointValue.setValueAsString(url, context); + } + + private static void invokeConfigureIdeOptions(TruststoreCommandlet commandlet, Path customTruststorePath) throws Exception { + + Method method = TruststoreCommandlet.class.getDeclaredMethod("configureIdeOptions", Path.class); + method.setAccessible(true); + method.invoke(commandlet, customTruststorePath); + } + + private void rememberSystemProperties() { + + this.previousTruststore = System.getProperty("javax.net.ssl.trustStore"); + this.previousTruststorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); + } + + private static void restoreSystemProperty(String key, String value) { + + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } + +} + + + + + + + diff --git a/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java new file mode 100644 index 000000000..10a2c1aa6 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java @@ -0,0 +1,195 @@ +package com.devonfw.tools.ide.truststore; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Enumeration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test of {@link TruststoreUtilImpl}. + */ +class TruststoreUtilImplTest { + + private static final String PASSWORD = "changeit"; + + private static final String TEST_CERT_RESOURCE = "/truststore/test-cert.pem"; + + @TempDir + Path tempDir; + + @Test + void testParseTlsEndpointFromHttpsUrl() { + + TruststoreUtilImpl.TlsEndpoint endpoint = TruststoreUtilImpl.parseTlsEndpoint("https://github.com/tools/path"); + + assertThat(endpoint.host()).isEqualTo("github.com"); + assertThat(endpoint.port()).isEqualTo(443); + } + + @Test + void testParseTlsEndpointFromHostAndPort() { + + TruststoreUtilImpl.TlsEndpoint endpoint = TruststoreUtilImpl.parseTlsEndpoint("my-host.local:8443"); + + assertThat(endpoint.host()).isEqualTo("my-host.local"); + assertThat(endpoint.port()).isEqualTo(8443); + } + + @Test + void testParseTlsEndpointRejectsHttp() { + + assertThatThrownBy(() -> TruststoreUtilImpl.parseTlsEndpoint("http://github.com")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only HTTPS URLs are supported"); + } + + @Test + void testIsTruststorePresent() { + + Path path = this.tempDir.resolve("truststore.p12"); + assertThat(TruststoreUtilImpl.isTruststorePresent(path)).isFalse(); + + writeEmptyTruststore(path); + + assertThat(TruststoreUtilImpl.isTruststorePresent(path)).isTrue(); + } + + @Test + void testCopyTruststore() throws Exception { + + X509Certificate certificate = loadCertificateFromResource(); + KeyStore source = KeyStore.getInstance("PKCS12"); + source.load(null, PASSWORD.toCharArray()); + source.setCertificateEntry("source-cert", certificate); + + KeyStore target = KeyStore.getInstance("PKCS12"); + target.load(null, PASSWORD.toCharArray()); + + TruststoreUtilImpl.copyTruststore(source, target); + + assertThat(target.getCertificate("source-cert")).isNotNull(); + } + + @Test + void testCreateOrUpdateTruststoreAddsCertificateOnlyOnce() throws Exception { + + Path truststorePath = this.tempDir.resolve("custom-existing.p12"); + writeEmptyTruststore(truststorePath); + + X509Certificate certificate = loadCertificateFromResource(); + + TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, certificate, "custom"); + int countAfterFirstAdd = countCertificateOccurrences(truststorePath, certificate); + + TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, certificate, "custom"); + int countAfterSecondAdd = countCertificateOccurrences(truststorePath, certificate); + + assertThat(countAfterFirstAdd).isEqualTo(1); + assertThat(countAfterSecondAdd).isEqualTo(1); + } + + @Test + void testCreateOrUpdateTruststoreCreatesFileIfMissing() throws Exception { + + Path truststorePath = this.tempDir.resolve("nested").resolve("custom-new.p12"); + + TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, loadCertificateFromResource(), "custom"); + + assertThat(truststorePath).exists(); + KeyStore truststore = loadTruststore(truststorePath); + assertThat(truststore.size()).isGreaterThan(0); + } + + @Test + void testDescribeCertificateContainsExpectedSections() throws Exception { + + X509Certificate certificate = loadCertificateFromResource(); + + String description = TruststoreUtilImpl.describeCertificate(certificate); + + assertThat(description).contains("Subject:"); + assertThat(description).contains("Issuer :"); + assertThat(description).contains("Serial :"); + assertThat(description).contains("SigAlg :"); + } + + @Test + void testFetchServerCertificateValidatesInput() { + + assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate(null, 443)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate(" ", 443)).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("host must not be blank"); + assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate("github.com", 0)).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("port must be between 1 and 65535"); + } + + @Test + void testLoadCertificateFromResource() throws Exception { + + X509Certificate certificate = loadCertificateFromResource(); + + assertThat(certificate.getSubjectX500Principal().getName()).contains("CN=IDEasy Test Cert"); + } + + private static X509Certificate loadCertificateFromResource() throws Exception { + + try (InputStream in = TruststoreUtilImplTest.class.getResourceAsStream(TEST_CERT_RESOURCE)) { + assertThat(in).as("Test certificate resource must exist: " + TEST_CERT_RESOURCE).isNotNull(); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(in); + } + } + + private static void writeEmptyTruststore(Path path) { + + try { + Files.createDirectories(path.getParent()); + KeyStore truststore = KeyStore.getInstance("PKCS12"); + truststore.load(null, PASSWORD.toCharArray()); + try (OutputStream out = Files.newOutputStream(path)) { + truststore.store(out, PASSWORD.toCharArray()); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to initialize empty truststore for test: " + path, e); + } + } + + private static KeyStore loadTruststore(Path truststorePath) throws Exception { + + KeyStore truststore = KeyStore.getInstance("PKCS12"); + try (InputStream in = Files.newInputStream(truststorePath)) { + truststore.load(in, PASSWORD.toCharArray()); + } + return truststore; + } + + private static int countCertificateOccurrences(Path truststorePath, X509Certificate certificate) throws Exception { + + KeyStore truststore = loadTruststore(truststorePath); + Enumeration aliases = truststore.aliases(); + int count = 0; + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (truststore.getCertificate(alias) instanceof X509Certificate existing && Arrays.equals(existing.getEncoded(), certificate.getEncoded())) { + count++; + } + } + return count; + } + +} + + + + diff --git a/cli/src/test/resources/truststore/test-cert.pem b/cli/src/test/resources/truststore/test-cert.pem new file mode 100644 index 000000000..3cd16017a --- /dev/null +++ b/cli/src/test/resources/truststore/test-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIJAOr4Zs6LtSLYMA0GCSqGSIb3DQEBDAUAMGcxCzAJBgNV +BAYTAkRFMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ8wDQYDVQQKEwZJ +REVhc3kxDjAMBgNVBAsTBVRlc3RzMRkwFwYDVQQDExBJREVhc3kgVGVzdCBDZXJ0 +MB4XDTI2MDMzMDA5MDE0N1oXDTM2MDMyNzA5MDE0N1owZzELMAkGA1UEBhMCREUx +DTALBgNVBAgTBFRlc3QxDTALBgNVBAcTBFRlc3QxDzANBgNVBAoTBklERWFzeTEO +MAwGA1UECxMFVGVzdHMxGTAXBgNVBAMTEElERWFzeSBUZXN0IENlcnQwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDgBKqwCWouR8arcukc14Q7LZ6ozwZP +RNzyFGx+v4pDBUY1QTlcYsQMpOUeyfsgo6WfX6ts743T95/Whh9EHmNP3jqad3o3 +tuWCWG6KyXtbv5BVUZa0qdgiVEjKizXaTW/IQiDMvNba+wnGzcH3k2V1J/1o9g0e +XABZdyG3eSg5XACpguWDXrHgq3P9+V5G0r72z215U5kB9bI5CQEocNEf0n3LFSrL +RvJ0+8Gm9ClsVRnsAw8IRaSsMO7A0K9lnvtnScbJr1zswBUMBOk3VqzdySXdxyeN +V/kO9nKuVY125r5sVDibo5uIIOdBWch6LkDIjrem1KKoHcCH5F22Q9kJAgMBAAGj +ITAfMB0GA1UdDgQWBBSBWH+GGyMGZpuktXrmuoOwZveR/jANBgkqhkiG9w0BAQwF +AAOCAQEAuFxxmJkKbQV0jRspb1yymgQRgL59+PEtWa0lDgkX4Zrk/8S/4B7+/rLR +8rRdlIFbSvAtmeUHCU59xto77N/hYGT0HLTDDbKBO5dYvEa3BKUZcHu/hBhSgp2A +0ggng3PH3p93gReEdvxFqDiP2Uf8wPj735JPQUiMAYPWGQ87jMss+nUmp5m5T8MQ +z5WrNg9QJG7Zg64qoK37oOmueCGtxBnyUtJ2ZvES5HS7cXLRwAezWvoYDFql2JQG +ghhJayeBO1iTM/SxnmWNg+dGGABqw9+00Tt2qE5sPsdOjaPVIcrqb2njPYUS5FTD +3xMnN+dAwg2lak+bV4crWvSEqV7psQ== +-----END CERTIFICATE----- + From 22b773a426c7680538fed83f92181c0385b9d200 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 30 Mar 2026 12:10:35 +0100 Subject: [PATCH 15/22] #1552: update changelog --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 7720dd614..765bf6fa6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -15,6 +15,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1770[#1770]: Fix setup hanging due to buffered log output during license prompt * https://github.com/devonfw/IDEasy/issues/1771[#1771]: Maven version 3.9.1x are now available * https://github.com/devonfw/IDEasy/issues/1687[#1687]: Fixed JLine warning about restricted method +* https://github.com/devonfw/IDEasy/issues/1552[#1552]: Add Commandlet to fix TLS issue The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/42?closed=1[milestone 2026.04.001]. From a937aac9a6714df0e5ed6eeb2a796420c24a4eb3 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Mon, 30 Mar 2026 12:26:52 +0100 Subject: [PATCH 16/22] #1552: log information if certificate related exception occurs --- .../tools/ide/network/NetworkStatusImpl.java | 18 +++++++---- .../ide/truststore/TruststoreUtilImpl.java | 30 +++++++++++++++++++ .../ide/commandlet/StatusCommandletTest.java | 22 +++++++++++++- .../truststore/TruststoreUtilImplTest.java | 14 +++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java index 77c13fb94..48878b570 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java @@ -4,7 +4,6 @@ import java.net.URL; import java.net.URLConnection; import java.util.concurrent.Callable; -import javax.net.ssl.SSLException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +12,7 @@ import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.truststore.TruststoreUtilImpl; /** * Implementation of {@link NetworkStatus}. @@ -113,10 +113,8 @@ public void logStatusMessage() { LOG.error(message); LOG.error(error.toString()); } - if (error instanceof SSLException) { - LOG.warn( - "You are having TLS issues. We guess you are forced to use a VPN tool breaking end-to-end encryption causing this effect. As a workaround you can create and configure a truststore as described here:"); - IdeLogLevel.INTERACTION.log(LOG, "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues"); + if (TruststoreUtilImpl.isTlsTrustIssue(error)) { + logTruststoreFixHint(); } else { IdeLogLevel.INTERACTION.log(LOG, "Please check potential proxy settings, ensure you are properly connected to the internet and retry this operation."); } @@ -133,9 +131,19 @@ public T invokeNetworkTask(Callable callable, String uri) { return callable.call(); } catch (IOException e) { this.onlineCheck.set(e); + if (TruststoreUtilImpl.isTlsTrustIssue(e)) { + logTruststoreFixHint(); + } throw new IllegalStateException("Network error whilst communicating to " + uri, e); } catch (Exception e) { throw new IllegalStateException("Unexpected checked exception whilst communicating to " + uri, e); } } + + private void logTruststoreFixHint() { + + LOG.warn( + "You are having TLS trust issues (PKIX/certificate-path/SSL handshake). As a workaround you can create and configure a truststore via 'ide fix-vpn-tls-problem ' (replace with the failing endpoint)."); + IdeLogLevel.INTERACTION.log(LOG, "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues"); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java b/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java index 3d1cbaaca..090302f5b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java @@ -29,6 +29,12 @@ */ public final class TruststoreUtilImpl { + private static final String ERROR_TEXT_PKIX = "pkix path building failed"; + + private static final String ERROR_TEXT_CERT_PATH = "unable to find valid certification path"; + + private static final String ERROR_TEXT_SSL_HANDSHAKE = "sslhandshakeexception"; + /** * Parsed TLS endpoint with host and port. * @@ -51,6 +57,30 @@ private TruststoreUtilImpl() { // utility class } + /** + * @param throwable the error to inspect. + * @return {@code true} if the error indicates a TLS trust/certificate path issue that can be fixed via truststore setup. + */ + public static boolean isTlsTrustIssue(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String message = current.getMessage(); + if (containsTlsTrustIndicator(message) || containsTlsTrustIndicator(current.getClass().getSimpleName())) { + return true; + } + current = current.getCause(); + } + return false; + } + + private static boolean containsTlsTrustIndicator(String text) { + if ((text == null) || text.isBlank()) { + return false; + } + String normalized = text.toLowerCase(Locale.ROOT); + return normalized.contains(ERROR_TEXT_PKIX) || normalized.contains(ERROR_TEXT_CERT_PATH) || normalized.contains(ERROR_TEXT_SSL_HANDSHAKE); + } + /** * Checks if a truststore file exists at the specified path. * diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java index c6315a365..1ba2efcef 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java @@ -96,10 +96,30 @@ void testStatusWhenTlsIssue() throws Exception { assertThat(context).log().hasEntries(IdeLogEntry.ofWarning("Skipping check for newer version of IDEasy because you are offline."), new IdeLogEntry(IdeLogLevel.ERROR, "You are offline because of the following error:", null, null, error, false), IdeLogEntry.ofWarning( - "You are having TLS issues. We guess you are forced to use a VPN tool breaking end-to-end encryption causing this effect. As a workaround you can create and configure a truststore as described here:"), + "You are having TLS trust issues (PKIX/certificate-path/SSL handshake). As a workaround you can create and configure a truststore via 'ide fix-vpn-tls-problem ' (replace with the failing endpoint)."), IdeLogEntry.ofInteraction("https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues")); } + /** + * Tests the output if {@link StatusCommandlet} is run and online-check error message contains known PKIX trust issue text. + */ + @Test + void testStatusWhenPkixIssueInMessage() { + + // arrange + IdeTestContext context = new IdeTestContext(); + IllegalStateException error = new IllegalStateException("unable to find valid certification path to requested target"); + context.getNetworkStatus().getOnlineCheck().set(error); + StatusCommandlet status = context.getCommandletManager().getCommandlet(StatusCommandlet.class); + + // act + status.run(); + + // assert + assertThat(context).logAtWarning().hasMessageContaining("via 'ide fix-vpn-tls-problem '"); + assertThat(context).logAtInteraction().hasMessageContaining("proxy-support.adoc#tls-certificate-issues"); + } + /** * Tests that the output of {@link StatusCommandlet} does not contain the username when run with active privacy mode on all OS (windows, linux, WSL, mac). * diff --git a/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java index 10a2c1aa6..85d7cc2aa 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java @@ -134,6 +134,20 @@ void testFetchServerCertificateValidatesInput() { .hasMessageContaining("port must be between 1 and 65535"); } + @Test + void testIsTlsTrustIssue() { + + Throwable pkix = new RuntimeException("PKIX path building failed"); + Throwable certPath = new RuntimeException("unable to find valid certification path"); + Throwable handshake = new IllegalStateException("failed", new RuntimeException("SSLHandshakeException")); + Throwable unrelated = new RuntimeException("Connection reset"); + + assertThat(TruststoreUtilImpl.isTlsTrustIssue(pkix)).isTrue(); + assertThat(TruststoreUtilImpl.isTlsTrustIssue(certPath)).isTrue(); + assertThat(TruststoreUtilImpl.isTlsTrustIssue(handshake)).isTrue(); + assertThat(TruststoreUtilImpl.isTlsTrustIssue(unrelated)).isFalse(); + } + @Test void testLoadCertificateFromResource() throws Exception { From 72a5eb10a9e4ee4a0a2788a988491c2273e46bd8 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Tue, 31 Mar 2026 12:46:37 +0100 Subject: [PATCH 17/22] #1552: applied requested PR changes --- .../ide/commandlet/TruststoreCommandlet.java | 57 ++++++++---- .../tools/ide/network/NetworkStatusImpl.java | 31 ++++++- .../TruststoreUtil.java} | 87 +++++++------------ cli/src/main/resources/nls/Help.properties | 5 +- cli/src/main/resources/nls/Help_de.properties | 5 +- .../ide/commandlet/StatusCommandletTest.java | 2 +- .../commandlet/TruststoreCommandletTest.java | 14 +-- ...lImplTest.java => TruststoreUtilTest.java} | 49 ++++------- 8 files changed, 131 insertions(+), 119 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/{truststore/TruststoreUtilImpl.java => util/TruststoreUtil.java} (88%) rename cli/src/test/java/com/devonfw/tools/ide/truststore/{TruststoreUtilImplTest.java => TruststoreUtilTest.java} (70%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java index eeebb2af7..eefd560dd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java @@ -2,6 +2,7 @@ import java.nio.file.Path; import java.security.cert.X509Certificate; +import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,11 +10,13 @@ import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.nls.NlsBundle; +import com.devonfw.tools.ide.property.EnumProperty; import com.devonfw.tools.ide.property.StringProperty; -import com.devonfw.tools.ide.truststore.TruststoreUtilImpl; +import com.devonfw.tools.ide.util.TruststoreUtil; /** * {@link Commandlet} to fix the TLS problem for VPN users. @@ -28,10 +31,10 @@ public class TruststoreCommandlet extends Commandlet { private static final String TRUSTSTORE_PASSWORD_OPTION_PREFIX = "-Djavax.net.ssl.trustStorePassword="; - private static final String DEFAULT_TRUSTSTORE_PASSWORD = "changeit"; - private final StringProperty url; + public final EnumProperty cfg; + /** * The constructor. * @@ -40,15 +43,20 @@ public class TruststoreCommandlet extends Commandlet { public TruststoreCommandlet(IdeContext context) { super(context); addKeyword(getName()); - this.url = add(new StringProperty("", true, "url")); + this.url = add(new StringProperty("", false, "url")); + this.cfg = add(new EnumProperty("--cfg", false, null, EnvironmentVariablesFiles.class)); } @Override public String getName() { - return "fix-vpn-tls-problem"; } + @Override + public boolean isIdeHomeRequired() { + return false; + } + /** * This commandlet tries to fix TLS problems for VPN users by capturing the untrusted certificate from the target endpoint and adding it to a custom * truststore. It also configures IDE_OPTIONS to use the custom truststore by default. The commandlet is idempotent and will not make changes if the endpoint @@ -70,26 +78,38 @@ public String getName() { protected void doRun() { String endpointInput = this.url.getValueAsString(); - TruststoreUtilImpl.TlsEndpoint endpoint; + boolean defaultUrlUsed = false; + + if (this.url.getValueAsString() == null || this.url.getValueAsString().isBlank()) { + endpointInput = "https://www.github.com"; + defaultUrlUsed = true; + } + + TruststoreUtil.TlsEndpoint endpoint; try { - endpoint = TruststoreUtilImpl.parseTlsEndpoint(endpointInput); + endpoint = TruststoreUtil.parseTlsEndpoint(endpointInput); } catch (IllegalArgumentException e) { throw new CliException("Invalid target URL/host '" + endpointInput + "': " + e.getMessage(), e); } String host = endpoint.host(); int port = endpoint.port(); - Path customTruststorePath = this.context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); + Path customTruststorePath = this.context.getUserHomeIde().resolve("truststore").resolve("truststore.p12"); - if (TruststoreUtilImpl.isTruststorePresent(customTruststorePath) && TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { + if (TruststoreUtil.isTruststorePresent(customTruststorePath) && TruststoreUtil.isReachable(host, port, customTruststorePath)) { IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath); configureIdeOptions(customTruststorePath); return; } - if (TruststoreUtilImpl.isReachable(host, port)) { - LOG.info("Successfully connected to {}:{} without certificate changes.", host, port); + if (TruststoreUtil.isReachable(host, port)) { + IdeLogLevel.SUCCESS.log(LOG, "Successfully connected to {}:{} without certificate changes.", host, port); LOG.info("No truststore update is required for the given address."); + if (defaultUrlUsed) { + LOG.info( + "If the issue still occurs try to call the command again and add the url that is causing the problem to the command: \n ide fix-vpn-tls-problem "); + } + return; } @@ -97,7 +117,7 @@ protected void doRun() { X509Certificate certificate; try { - certificate = TruststoreUtilImpl.fetchServerCertificate(host, port); + certificate = TruststoreUtil.fetchServerCertificate(host, port); } catch (Exception e) { LOG.error("Failed to capture certificate from {}:{}.", host, port, e); IdeLogLevel.INTERACTION.log(LOG, @@ -106,7 +126,7 @@ protected void doRun() { } LOG.info("Captured untrusted certificate:"); - LOG.info(TruststoreUtilImpl.describeCertificate(certificate)); + LOG.info(TruststoreUtil.describeCertificate(certificate)); boolean addToTruststore = this.context.question("Do you want to add this certificate to the custom truststore at {}?", customTruststorePath); @@ -116,7 +136,7 @@ protected void doRun() { } try { - TruststoreUtilImpl.createOrUpdateTruststore(customTruststorePath, certificate, "custom"); + TruststoreUtil.createOrUpdateTruststore(customTruststorePath, certificate, "custom"); IdeLogLevel.SUCCESS.log(LOG, "Custom truststore updated at {}", customTruststorePath); } catch (Exception e) { LOG.error("Failed to create or update custom truststore at {}", customTruststorePath, e); @@ -125,7 +145,7 @@ protected void doRun() { configureIdeOptions(customTruststorePath); - if (TruststoreUtilImpl.isReachable(host, port, customTruststorePath)) { + if (TruststoreUtil.isReachable(host, port, customTruststorePath)) { IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with custom truststore."); } else { LOG.warn("TLS handshake still fails even with custom truststore."); @@ -135,9 +155,10 @@ protected void doRun() { private void configureIdeOptions(Path customTruststorePath) { String truststorePath = customTruststorePath.toAbsolutePath().toString(); String truststoreOption = TRUSTSTORE_OPTION_PREFIX + truststorePath; - String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + DEFAULT_TRUSTSTORE_PASSWORD; + String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + Arrays.toString(TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD); + + EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.USER); - EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.CONF); if (confVariables == null) { IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption); return; @@ -154,7 +175,7 @@ private void configureIdeOptions(Path customTruststorePath) { confVariables.save(); // Apply directly for the current process as well. System.setProperty("javax.net.ssl.trustStore", truststorePath); - System.setProperty("javax.net.ssl.trustStorePassword", DEFAULT_TRUSTSTORE_PASSWORD); + System.setProperty("javax.net.ssl.trustStorePassword", Arrays.toString(TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD)); IdeLogLevel.SUCCESS.log(LOG, "IDE_OPTIONS configured to use custom truststore by default."); } catch (UnsupportedOperationException e) { IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption); diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java index 48878b570..40ceccdc0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.net.URL; import java.net.URLConnection; +import java.util.Locale; import java.util.concurrent.Callable; import org.slf4j.Logger; @@ -12,7 +13,6 @@ import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.truststore.TruststoreUtilImpl; /** * Implementation of {@link NetworkStatus}. @@ -29,6 +29,8 @@ public class NetworkStatusImpl implements NetworkStatus { protected final CachedValue onlineCheck; + private static final String ERROR_TEXT_PKIX = "pkix path building failed"; + /** * @param ideContext the {@link AbstractIdeContext}. */ @@ -113,7 +115,7 @@ public void logStatusMessage() { LOG.error(message); LOG.error(error.toString()); } - if (TruststoreUtilImpl.isTlsTrustIssue(error)) { + if (isTlsTrustIssue(error)) { logTruststoreFixHint(); } else { IdeLogLevel.INTERACTION.log(LOG, "Please check potential proxy settings, ensure you are properly connected to the internet and retry this operation."); @@ -131,7 +133,7 @@ public T invokeNetworkTask(Callable callable, String uri) { return callable.call(); } catch (IOException e) { this.onlineCheck.set(e); - if (TruststoreUtilImpl.isTlsTrustIssue(e)) { + if (isTlsTrustIssue(e)) { logTruststoreFixHint(); } throw new IllegalStateException("Network error whilst communicating to " + uri, e); @@ -143,7 +145,28 @@ public T invokeNetworkTask(Callable callable, String uri) { private void logTruststoreFixHint() { LOG.warn( - "You are having TLS trust issues (PKIX/certificate-path/SSL handshake). As a workaround you can create and configure a truststore via 'ide fix-vpn-tls-problem ' (replace with the failing endpoint)."); + "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 with the failing endpoint):\nide fix-vpn-tls-problem "); IdeLogLevel.INTERACTION.log(LOG, "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues"); } + + boolean isTlsTrustIssue(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String message = current.getMessage(); + if (containsTlsTrustIndicator(message) || containsTlsTrustIndicator(current.getClass().getSimpleName())) { + return true; + } + current = current.getCause(); + } + return false; + } + + boolean containsTlsTrustIndicator(String text) { + if ((text == null) || text.isBlank()) { + return false; + } + String normalized = text.toLowerCase(Locale.ROOT); + return normalized.contains(ERROR_TEXT_PKIX); + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java b/cli/src/main/java/com/devonfw/tools/ide/util/TruststoreUtil.java similarity index 88% rename from cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java rename to cli/src/main/java/com/devonfw/tools/ide/util/TruststoreUtil.java index 090302f5b..13b530ef3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/truststore/TruststoreUtilImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/util/TruststoreUtil.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.truststore; +package com.devonfw.tools.ide.util; import java.io.InputStream; import java.io.OutputStream; @@ -27,13 +27,7 @@ /** * Utility methods for truststore handling and TLS certificate capture. */ -public final class TruststoreUtilImpl { - - private static final String ERROR_TEXT_PKIX = "pkix path building failed"; - - private static final String ERROR_TEXT_CERT_PATH = "unable to find valid certification path"; - - private static final String ERROR_TEXT_SSL_HANDSHAKE = "sslhandshakeexception"; +public final class TruststoreUtil { /** * Parsed TLS endpoint with host and port. @@ -45,40 +39,29 @@ public record TlsEndpoint(String host, int port) { } - public static final char[] DEFAULT_CACERTS_PASSWORD = "changeit".toCharArray(); - - public static final char[] CUSTOM_TRUSTSTORE_PASSWORD = "changeit".toCharArray(); - - public static final String DEFAULT_ALIAS_PREFIX = "custom"; + private static final String TRUSTSTORE_PASSWORD = "changeit"; - private static final int DEFAULT_TIMEOUT_MILLIS = 10_000; + /** + * Default password for the JRE cacerts truststore + */ + public static final char[] DEFAULT_CACERTS_PASSWORD = TRUSTSTORE_PASSWORD.toCharArray(); - private TruststoreUtilImpl() { - // utility class - } + /** + * Password for the custom truststore + */ + public static final char[] CUSTOM_TRUSTSTORE_PASSWORD = TRUSTSTORE_PASSWORD.toCharArray(); /** - * @param throwable the error to inspect. - * @return {@code true} if the error indicates a TLS trust/certificate path issue that can be fixed via truststore setup. + * Default prefix for aliases of certificates added to the truststore. */ - public static boolean isTlsTrustIssue(Throwable throwable) { - Throwable current = throwable; - while (current != null) { - String message = current.getMessage(); - if (containsTlsTrustIndicator(message) || containsTlsTrustIndicator(current.getClass().getSimpleName())) { - return true; - } - current = current.getCause(); - } - return false; - } + private static final String DEFAULT_ALIAS_PREFIX = "custom"; - private static boolean containsTlsTrustIndicator(String text) { - if ((text == null) || text.isBlank()) { - return false; - } - String normalized = text.toLowerCase(Locale.ROOT); - return normalized.contains(ERROR_TEXT_PKIX) || normalized.contains(ERROR_TEXT_CERT_PATH) || normalized.contains(ERROR_TEXT_SSL_HANDSHAKE); + private static final int DEFAULT_TIMEOUT_MILLIS = 10_000; + + private static final String TLS_PROTOCOL = "TLS"; + + private TruststoreUtil() { + // utility class } /** @@ -269,25 +252,19 @@ private static void validateEndpoint(String host, int port, String input) { */ public static boolean isReachable(String host, int port) { validateEndpoint(host, port, host + ":" + port); - SSLSocket socket = null; try { - SSLContext sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL); sslContext.init(null, null, new SecureRandom()); SSLSocketFactory factory = sslContext.getSocketFactory(); - socket = connectTlsSocket(factory, host, port); - socket.startHandshake(); + + try (SSLSocket socket = connectTlsSocket(factory, host, port)) { + socket.startHandshake(); + } return true; } catch (Exception e) { return false; - } finally { - if (socket != null) { - try { - socket.close(); - } catch (Exception ignored) { - // ignore close failure after reachability probe - } - } } + } /** @@ -346,16 +323,14 @@ public static X509Certificate fetchServerCertificate(String host, int port) thro SavingTrustManager savingTrustManager = new SavingTrustManager(); - SSLContext sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL); sslContext.init(null, new TrustManager[] { savingTrustManager }, new SecureRandom()); SSLSocketFactory factory = sslContext.getSocketFactory(); try (SSLSocket socket = connectTlsSocket(factory, host, port)) { - try { - socket.startHandshake(); - } catch (SSLException e) { - // expected: trust manager aborts after capturing the chain - } + socket.startHandshake(); + } catch (SSLException e) { + // expected: trust manager aborts after capturing the chain } X509Certificate[] chain = savingTrustManager.getChain(); @@ -385,7 +360,7 @@ public static void verifyConnectionWithTruststore(String host, int port, Path tr TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(truststore); - SSLContext sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL); sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); SSLSocketFactory socketFactory = sslContext.getSocketFactory(); @@ -403,7 +378,7 @@ public static void verifyConnectionWithTruststore(String host, int port, Path tr * @return a human-readable description of the given X.509 certificate. */ public static String describeCertificate(X509Certificate certificate) { - String nl = System.lineSeparator(); + String nl = "\n"; StringBuilder sb = new StringBuilder(); sb.append("Subject: ").append(certificate.getSubjectX500Principal()).append(nl); sb.append("Issuer : ").append(certificate.getIssuerX500Principal()).append(nl); diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index cbb454880..989806fc3 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -24,8 +24,9 @@ cmd.env=Prints the environment variables to set and export. cmd.env.detail=To print all active environment variables of IDEasy simply type: 'ide env'. If you add the '--debug' flag to ide e.g. 'ide --debug env' IDEasy will also add additional information about the locations of the definitions to the output. cmd.env.opt.--bash=Convert Windows path syntax to bash for usage in git-bash. cmd.fix-vpn-tls-problem=Commandlet to fix the VPN TLS problem on Windows. -cmd.fix-vpn-tls-problem.detail=If you are using a VPN on Windows and encounter TLS problems, this commandlet can help you fix the issue by adding the necessary certificates to your Java keystore. Simply run 'ide fix-vpn-tls-problem' and follow the instructions provided in the console output. -cmd.fix-vpn-tls-problem.val.url=The URL that is affected by the VPN TLS problem (e.g. 'https://api.azul.com'). +cmd.fix-vpn-tls-problem.detail=If you are using a VPN on Windows and encounter TLS problems, this commandlet can help you fix the issue by adding the necessary certificates to your Java keystore. Simply run the following command:\nide fix-vpn-tls-problem +cmd.fix-vpn-tls-problem.opt.--cfg=Selection of the configuration file (settings | home | conf | workspace). +cmd.fix-vpn-tls-problem.val.url=The URL that is affected by the VPN TLS problem (e.g. 'https://api.azul.com'), if not provided the commandlet will test 'https://github.com' by default. cmd.gcviewer=Tool commandlet for GC Viewer (View garbage collector logs of Java). cmd.gcviewer.detail=GCViewer is a tool for analyzing and visualizing Java garbage collection logs. Detailed documentation can be found at https://github.com/chewiebug/GCViewer cmd.get-edition=Get the edition of the selected tool. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index d72d04a64..cb7ad0385 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -24,8 +24,9 @@ cmd.env=Gibt die zu setzenden und exportierenden Umgebungsvariablen aus. cmd.env.detail=Um alle aktiven Umgebungsvariablen von IDEasy auszugeben, geben Sie einfach 'ide env' in die Konsole ein. Um zusätzlich noch die Ursprünge der Variablen ausgegeben zu bekommen, fügen Sie einfach das debug flag '--debug' hinzu z.B. 'ide --debug env'. cmd.env.opt.--bash=Konvertiert Windows-Pfad-Syntax nach Bash zur Verwendung in git-bash. cmd.fix-vpn-tls-problem=Wekzeug Kommando zum Beheben von VPN TLS Problemen auf Windows -cmd.fix-vpn-tls-problem.detail=Auf einigen Windows-Systemen kann es zu Problemen mit der TLS-Verbindung kommen, wenn eine VPN-Verbindung aktiv ist. Dieses Problem kann dazu führen, dass bestimmte Werkzeuge oder Dienste nicht ordnungsgemäß funktionieren. Das Kommando 'ide fix-vpn-tls-problem' bietet eine Lösung für dieses Problem, indem es die TLS-Einstellungen anpasst, um die Kompatibilität mit VPN-Verbindungen zu verbessern. Um dieses Problem zu beheben, führen Sie einfach den folgenden Befehl aus: 'ide fix-vpn-tls-problem --url '. Ersetzen Sie durch die URL, die von dem VPN TLS Problem betroffen ist (z.B. 'https://api.azul.com'). -cmd.fix-vpn-tls-problem.val.url=Die URL, die von dem VPN TLS Problem betroffen ist (z.B. 'https://api.azul.com'). +cmd.fix-vpn-tls-problem.detail=Auf einigen Windows-Systemen kann es zu Problemen mit der TLS-Verbindung kommen, wenn eine VPN-Verbindung aktiv ist. Dieses Werkzeug Kommando bietet eine Lösung für dieses Problem, indem es die TLS-Einstellungen anpasst, um die Kompatibilität mit VPN-Verbindungen zu verbessern. Um dieses Problem zu beheben, führen Sie einfach den folgenden Befehl aus:\nide fix-vpn-tls-problem +cmd.fix-vpn-tls-problem.opt.--cfg=Auswahl der Konfigurationsdatei (settings | home | conf | workspace). +cmd.fix-vpn-tls-problem.val.url=Die URL, die von dem VPN TLS Problem betroffen ist (e.g. 'https://api.azul.com'). Ohne eingabe einer URL wird standardmäßig: 'https://github.com' angefragt. cmd.gcviewer=Werkzeug Kommando für GC Viewer (Anzeige von Garbage-Collector Logs von Java). cmd.gcviewer.detail=GCViewer ist ein Tool zur Analyse und Visualisierung von Java-Garbage-Collection-Protokollen. Detaillierte Dokumentation ist zu finden unter https://github.com/chewiebug/GCViewer cmd.get-edition=Zeigt die Edition des selektierten Werkzeugs an. diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java index 1ba2efcef..0c6f9d985 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java @@ -96,7 +96,7 @@ void testStatusWhenTlsIssue() throws Exception { assertThat(context).log().hasEntries(IdeLogEntry.ofWarning("Skipping check for newer version of IDEasy because you are offline."), new IdeLogEntry(IdeLogLevel.ERROR, "You are offline because of the following error:", null, null, error, false), IdeLogEntry.ofWarning( - "You are having TLS trust issues (PKIX/certificate-path/SSL handshake). As a workaround you can create and configure a truststore via 'ide fix-vpn-tls-problem ' (replace with the failing endpoint)."), + "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 with the failing endpoint):\nide fix-vpn-tls-problem "), IdeLogEntry.ofInteraction("https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues")); } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java index 111063774..0029a6989 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java @@ -2,6 +2,7 @@ import java.lang.reflect.Method; import java.nio.file.Path; +import java.util.Arrays; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -13,6 +14,7 @@ import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.property.Property; +import com.devonfw.tools.ide.util.TruststoreUtil; /** * Test of {@link TruststoreCommandlet}. @@ -68,23 +70,25 @@ void testConfigureIdeOptionsReplacesExistingTruststoreOptions(@TempDir Path temp TruststoreCommandlet commandlet = context.getCommandletManager().getCommandlet(TruststoreCommandlet.class); rememberSystemProperties(); - EnvironmentVariables confVariables = context.getVariables().getByType(EnvironmentVariablesType.CONF); - confVariables.set(IDE_OPTIONS, + EnvironmentVariables userVariables = context.getVariables().getByType(EnvironmentVariablesType.USER); + userVariables.set(IDE_OPTIONS, "-Xmx512m -Djavax.net.ssl.trustStore=/old/path.p12 -Djavax.net.ssl.trustStorePassword=old-secret"); Path newTruststorePath = context.getSettingsPath().resolve("truststore").resolve("truststore.p12"); invokeConfigureIdeOptions(commandlet, newTruststorePath); - String options = confVariables.getFlat(IDE_OPTIONS); + String options = userVariables.getFlat(IDE_OPTIONS); + String expectedPassword = Arrays.toString(TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD); + assertThat(options).contains("-Xmx512m"); assertThat(options).contains("-Djavax.net.ssl.trustStore=" + newTruststorePath.toAbsolutePath()); - assertThat(options).contains("-Djavax.net.ssl.trustStorePassword=" + TRUSTSTORE_PASSWORD); + assertThat(options).contains("-Djavax.net.ssl.trustStorePassword=" + expectedPassword); assertThat(options).doesNotContain("/old/path.p12"); assertThat(options).doesNotContain("old-secret"); assertThat(System.getProperty("javax.net.ssl.trustStore")).isEqualTo(newTruststorePath.toAbsolutePath().toString()); - assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo(TRUSTSTORE_PASSWORD); + assertThat(System.getProperty("javax.net.ssl.trustStorePassword")).isEqualTo(expectedPassword); } private IdeTestContext newIsolatedContext(Path tempDir) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilTest.java similarity index 70% rename from cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java rename to cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilTest.java index 85d7cc2aa..79a75c4da 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/truststore/TruststoreUtilTest.java @@ -16,10 +16,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import com.devonfw.tools.ide.util.TruststoreUtil; + /** - * Test of {@link TruststoreUtilImpl}. + * Test of {@link TruststoreUtil}. */ -class TruststoreUtilImplTest { +class TruststoreUtilTest { private static final String PASSWORD = "changeit"; @@ -31,7 +33,7 @@ class TruststoreUtilImplTest { @Test void testParseTlsEndpointFromHttpsUrl() { - TruststoreUtilImpl.TlsEndpoint endpoint = TruststoreUtilImpl.parseTlsEndpoint("https://github.com/tools/path"); + TruststoreUtil.TlsEndpoint endpoint = TruststoreUtil.parseTlsEndpoint("https://github.com/tools/path"); assertThat(endpoint.host()).isEqualTo("github.com"); assertThat(endpoint.port()).isEqualTo(443); @@ -40,7 +42,7 @@ void testParseTlsEndpointFromHttpsUrl() { @Test void testParseTlsEndpointFromHostAndPort() { - TruststoreUtilImpl.TlsEndpoint endpoint = TruststoreUtilImpl.parseTlsEndpoint("my-host.local:8443"); + TruststoreUtil.TlsEndpoint endpoint = TruststoreUtil.parseTlsEndpoint("my-host.local:8443"); assertThat(endpoint.host()).isEqualTo("my-host.local"); assertThat(endpoint.port()).isEqualTo(8443); @@ -49,8 +51,7 @@ void testParseTlsEndpointFromHostAndPort() { @Test void testParseTlsEndpointRejectsHttp() { - assertThatThrownBy(() -> TruststoreUtilImpl.parseTlsEndpoint("http://github.com")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> TruststoreUtil.parseTlsEndpoint("http://github.com")).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Only HTTPS URLs are supported"); } @@ -58,11 +59,11 @@ void testParseTlsEndpointRejectsHttp() { void testIsTruststorePresent() { Path path = this.tempDir.resolve("truststore.p12"); - assertThat(TruststoreUtilImpl.isTruststorePresent(path)).isFalse(); + assertThat(TruststoreUtil.isTruststorePresent(path)).isFalse(); writeEmptyTruststore(path); - assertThat(TruststoreUtilImpl.isTruststorePresent(path)).isTrue(); + assertThat(TruststoreUtil.isTruststorePresent(path)).isTrue(); } @Test @@ -76,7 +77,7 @@ void testCopyTruststore() throws Exception { KeyStore target = KeyStore.getInstance("PKCS12"); target.load(null, PASSWORD.toCharArray()); - TruststoreUtilImpl.copyTruststore(source, target); + TruststoreUtil.copyTruststore(source, target); assertThat(target.getCertificate("source-cert")).isNotNull(); } @@ -89,10 +90,10 @@ void testCreateOrUpdateTruststoreAddsCertificateOnlyOnce() throws Exception { X509Certificate certificate = loadCertificateFromResource(); - TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, certificate, "custom"); + TruststoreUtil.createOrUpdateTruststore(truststorePath, certificate, "custom"); int countAfterFirstAdd = countCertificateOccurrences(truststorePath, certificate); - TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, certificate, "custom"); + TruststoreUtil.createOrUpdateTruststore(truststorePath, certificate, "custom"); int countAfterSecondAdd = countCertificateOccurrences(truststorePath, certificate); assertThat(countAfterFirstAdd).isEqualTo(1); @@ -104,7 +105,7 @@ void testCreateOrUpdateTruststoreCreatesFileIfMissing() throws Exception { Path truststorePath = this.tempDir.resolve("nested").resolve("custom-new.p12"); - TruststoreUtilImpl.createOrUpdateTruststore(truststorePath, loadCertificateFromResource(), "custom"); + TruststoreUtil.createOrUpdateTruststore(truststorePath, loadCertificateFromResource(), "custom"); assertThat(truststorePath).exists(); KeyStore truststore = loadTruststore(truststorePath); @@ -116,7 +117,7 @@ void testDescribeCertificateContainsExpectedSections() throws Exception { X509Certificate certificate = loadCertificateFromResource(); - String description = TruststoreUtilImpl.describeCertificate(certificate); + String description = TruststoreUtil.describeCertificate(certificate); assertThat(description).contains("Subject:"); assertThat(description).contains("Issuer :"); @@ -127,27 +128,13 @@ void testDescribeCertificateContainsExpectedSections() throws Exception { @Test void testFetchServerCertificateValidatesInput() { - assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate(null, 443)).isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate(" ", 443)).isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> TruststoreUtil.fetchServerCertificate(null, 443)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> TruststoreUtil.fetchServerCertificate(" ", 443)).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("host must not be blank"); - assertThatThrownBy(() -> TruststoreUtilImpl.fetchServerCertificate("github.com", 0)).isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> TruststoreUtil.fetchServerCertificate("github.com", 0)).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("port must be between 1 and 65535"); } - @Test - void testIsTlsTrustIssue() { - - Throwable pkix = new RuntimeException("PKIX path building failed"); - Throwable certPath = new RuntimeException("unable to find valid certification path"); - Throwable handshake = new IllegalStateException("failed", new RuntimeException("SSLHandshakeException")); - Throwable unrelated = new RuntimeException("Connection reset"); - - assertThat(TruststoreUtilImpl.isTlsTrustIssue(pkix)).isTrue(); - assertThat(TruststoreUtilImpl.isTlsTrustIssue(certPath)).isTrue(); - assertThat(TruststoreUtilImpl.isTlsTrustIssue(handshake)).isTrue(); - assertThat(TruststoreUtilImpl.isTlsTrustIssue(unrelated)).isFalse(); - } - @Test void testLoadCertificateFromResource() throws Exception { @@ -158,7 +145,7 @@ void testLoadCertificateFromResource() throws Exception { private static X509Certificate loadCertificateFromResource() throws Exception { - try (InputStream in = TruststoreUtilImplTest.class.getResourceAsStream(TEST_CERT_RESOURCE)) { + try (InputStream in = TruststoreUtilTest.class.getResourceAsStream(TEST_CERT_RESOURCE)) { assertThat(in).as("Test certificate resource must exist: " + TEST_CERT_RESOURCE).isNotNull(); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); return (X509Certificate) certificateFactory.generateCertificate(in); From ea6dc23f65b5ced52065c9f15f38c7181ef80054 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Tue, 31 Mar 2026 12:48:51 +0100 Subject: [PATCH 18/22] #1552: updated test --- .../com/devonfw/tools/ide/commandlet/StatusCommandletTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java index 0c6f9d985..3a57e41d7 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java @@ -116,7 +116,7 @@ void testStatusWhenPkixIssueInMessage() { status.run(); // assert - assertThat(context).logAtWarning().hasMessageContaining("via 'ide fix-vpn-tls-problem '"); + assertThat(context).logAtWarning().hasMessageContaining("'ide fix-vpn-tls-problem '"); assertThat(context).logAtInteraction().hasMessageContaining("proxy-support.adoc#tls-certificate-issues"); } From 2dbe39da30cc3c163dc2381de08ca24b60dd604f Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Tue, 31 Mar 2026 13:18:36 +0100 Subject: [PATCH 19/22] #1552: update test --- .../ide/commandlet/StatusCommandletTest.java | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java index 3a57e41d7..69ecfb01d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/StatusCommandletTest.java @@ -94,8 +94,7 @@ void testStatusWhenTlsIssue() throws Exception { // assert assertThat(context).log().hasEntries(IdeLogEntry.ofWarning("Skipping check for newer version of IDEasy because you are offline."), - new IdeLogEntry(IdeLogLevel.ERROR, "You are offline because of the following error:", null, null, error, false), - IdeLogEntry.ofWarning( + new IdeLogEntry(IdeLogLevel.ERROR, "You are offline because of the following error:", null, null, error, false), IdeLogEntry.ofWarning( "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 with the failing endpoint):\nide fix-vpn-tls-problem "), IdeLogEntry.ofInteraction("https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues")); } @@ -108,7 +107,7 @@ void testStatusWhenPkixIssueInMessage() { // arrange IdeTestContext context = new IdeTestContext(); - IllegalStateException error = new IllegalStateException("unable to find valid certification path to requested target"); + IllegalStateException error = new IllegalStateException("PKIX path building failed: unable to find valid certification path to requested target"); context.getNetworkStatus().getOnlineCheck().set(error); StatusCommandlet status = context.getCommandletManager().getCommandlet(StatusCommandlet.class); @@ -116,7 +115,7 @@ void testStatusWhenPkixIssueInMessage() { status.run(); // assert - assertThat(context).logAtWarning().hasMessageContaining("'ide fix-vpn-tls-problem '"); + assertThat(context).logAtWarning().hasMessageContaining("ide fix-vpn-tls-problem "); assertThat(context).logAtInteraction().hasMessageContaining("proxy-support.adoc#tls-certificate-issues"); } @@ -151,30 +150,9 @@ void testStatusWhenInPrivacyMode(String os, Path ideHome, Path ideRoot, Path use private static Stream providePrivacyModeTestCases() { return Stream.of( - Arguments.of( - "linux", - Path.of("/mnt/c/Users/testuser/projects/myproject"), - Path.of("/mnt/c/Users/testuser/projects"), - Path.of("/mnt/c/projects") - ), - Arguments.of( - "windows", - Path.of("C:\\Users\\testuser\\projects\\myproject"), - Path.of("C:\\Users\\testuser\\projects"), - Path.of("C:\\Users\\testuser") - ), - Arguments.of( - "linux", - Path.of("/home/testuser/projects/myproject"), - Path.of("/home/testuser/projects"), - Path.of("/home/testuser") - ), - Arguments.of( - "mac", - Path.of("/Users/testuser/projects/myproject"), - Path.of("/Users/testuser/projects"), - Path.of("/Users/testuser") - ) - ); + Arguments.of("linux", Path.of("/mnt/c/Users/testuser/projects/myproject"), Path.of("/mnt/c/Users/testuser/projects"), Path.of("/mnt/c/projects")), + Arguments.of("windows", Path.of("C:\\Users\\testuser\\projects\\myproject"), Path.of("C:\\Users\\testuser\\projects"), Path.of("C:\\Users\\testuser")), + Arguments.of("linux", Path.of("/home/testuser/projects/myproject"), Path.of("/home/testuser/projects"), Path.of("/home/testuser")), + Arguments.of("mac", Path.of("/Users/testuser/projects/myproject"), Path.of("/Users/testuser/projects"), Path.of("/Users/testuser"))); } } From f8e194ec941335254df0896576d4f3a253fd44d0 Mon Sep 17 00:00:00 2001 From: Marvin Meitzner Date: Thu, 2 Apr 2026 13:03:17 +0100 Subject: [PATCH 20/22] #1552: requested changes from pr --- .../tools/ide/commandlet/TruststoreCommandlet.java | 13 +------------ cli/src/main/resources/nls/Help.properties | 1 - cli/src/main/resources/nls/Help_de.properties | 1 - .../ide/commandlet/TruststoreCommandletTest.java | 1 - 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java index eefd560dd..fded542d5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java @@ -10,11 +10,8 @@ import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; -import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.nls.NlsBundle; -import com.devonfw.tools.ide.property.EnumProperty; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.util.TruststoreUtil; @@ -33,7 +30,6 @@ public class TruststoreCommandlet extends Commandlet { private final StringProperty url; - public final EnumProperty cfg; /** * The constructor. @@ -44,7 +40,6 @@ public TruststoreCommandlet(IdeContext context) { super(context); addKeyword(getName()); this.url = add(new StringProperty("", false, "url")); - this.cfg = add(new EnumProperty("--cfg", false, null, EnvironmentVariablesFiles.class)); } @Override @@ -80,7 +75,7 @@ protected void doRun() { String endpointInput = this.url.getValueAsString(); boolean defaultUrlUsed = false; - if (this.url.getValueAsString() == null || this.url.getValueAsString().isBlank()) { + if (endpointInput == null || endpointInput.isBlank()) { endpointInput = "https://www.github.com"; defaultUrlUsed = true; } @@ -205,10 +200,4 @@ private static String appendOption(String options, String option) { } return options + " " + option; } - - @Override - public void printHelp(NlsBundle bundle) { - LOG.info( - "This commandlet helps to fix TLS issues for users behind VPNs by capturing untrusted certificates from target endpoints and adding them to a custom truststore. It also configures IDE_OPTIONS to use the custom truststore by default. The commandlet is idempotent and will not make changes if the endpoint is already reachable or if the certificate is already trusted."); - } } diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 989806fc3..a516042c8 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -25,7 +25,6 @@ cmd.env.detail=To print all active environment variables of IDEasy simply type: cmd.env.opt.--bash=Convert Windows path syntax to bash for usage in git-bash. cmd.fix-vpn-tls-problem=Commandlet to fix the VPN TLS problem on Windows. cmd.fix-vpn-tls-problem.detail=If you are using a VPN on Windows and encounter TLS problems, this commandlet can help you fix the issue by adding the necessary certificates to your Java keystore. Simply run the following command:\nide fix-vpn-tls-problem -cmd.fix-vpn-tls-problem.opt.--cfg=Selection of the configuration file (settings | home | conf | workspace). cmd.fix-vpn-tls-problem.val.url=The URL that is affected by the VPN TLS problem (e.g. 'https://api.azul.com'), if not provided the commandlet will test 'https://github.com' by default. cmd.gcviewer=Tool commandlet for GC Viewer (View garbage collector logs of Java). cmd.gcviewer.detail=GCViewer is a tool for analyzing and visualizing Java garbage collection logs. Detailed documentation can be found at https://github.com/chewiebug/GCViewer diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index cb7ad0385..124001ed4 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -25,7 +25,6 @@ cmd.env.detail=Um alle aktiven Umgebungsvariablen von IDEasy auszugeben, geben S cmd.env.opt.--bash=Konvertiert Windows-Pfad-Syntax nach Bash zur Verwendung in git-bash. cmd.fix-vpn-tls-problem=Wekzeug Kommando zum Beheben von VPN TLS Problemen auf Windows cmd.fix-vpn-tls-problem.detail=Auf einigen Windows-Systemen kann es zu Problemen mit der TLS-Verbindung kommen, wenn eine VPN-Verbindung aktiv ist. Dieses Werkzeug Kommando bietet eine Lösung für dieses Problem, indem es die TLS-Einstellungen anpasst, um die Kompatibilität mit VPN-Verbindungen zu verbessern. Um dieses Problem zu beheben, führen Sie einfach den folgenden Befehl aus:\nide fix-vpn-tls-problem -cmd.fix-vpn-tls-problem.opt.--cfg=Auswahl der Konfigurationsdatei (settings | home | conf | workspace). cmd.fix-vpn-tls-problem.val.url=Die URL, die von dem VPN TLS Problem betroffen ist (e.g. 'https://api.azul.com'). Ohne eingabe einer URL wird standardmäßig: 'https://github.com' angefragt. cmd.gcviewer=Werkzeug Kommando für GC Viewer (Anzeige von Garbage-Collector Logs von Java). cmd.gcviewer.detail=GCViewer ist ein Tool zur Analyse und Visualisierung von Java-Garbage-Collection-Protokollen. Detaillierte Dokumentation ist zu finden unter https://github.com/chewiebug/GCViewer diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java index 0029a6989..33d655813 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TruststoreCommandletTest.java @@ -23,7 +23,6 @@ class TruststoreCommandletTest extends AbstractIdeContextTest { private static final String IDE_OPTIONS = "IDE_OPTIONS"; - private static final String TRUSTSTORE_PASSWORD = "changeit"; private String previousTruststore; From bc30f54b848e700f1e70b95327a60b08586ce54a Mon Sep 17 00:00:00 2001 From: MarvMa Date: Tue, 7 Apr 2026 09:11:25 +0100 Subject: [PATCH 21/22] Update cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requested changes Co-authored-by: Jörg Hohwiller --- .../java/com/devonfw/tools/ide/network/NetworkStatusImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java index 40ceccdc0..405d24094 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkStatusImpl.java @@ -153,7 +153,7 @@ boolean isTlsTrustIssue(Throwable throwable) { Throwable current = throwable; while (current != null) { String message = current.getMessage(); - if (containsTlsTrustIndicator(message) || containsTlsTrustIndicator(current.getClass().getSimpleName())) { + if (containsTlsTrustIndicator(message)) { return true; } current = current.getCause(); From d94dcce6d61e228b410e09ef59c8d980654d9e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Tue, 7 Apr 2026 14:51:47 +0200 Subject: [PATCH 22/22] fixed CHANGELOG Updated CHANGELOG with new features and bugfixes for release 2026.04.002. --- CHANGELOG.adoc | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index d6348f1b2..d3b6a30b7 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,8 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1552[#1552]: Add Commandlet to fix TLS issue + The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/43?closed=1[milestone 2026.04.002]. == 2026.04.001 @@ -15,13 +17,6 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1702[#1702]: UI Buttons do not scale properly with window resize * https://github.com/devonfw/IDEasy/issues/1751[#1751]: Add go commandlet (go-lang support) * https://github.com/devonfw/IDEasy/issues/1732[#1732]: Add stash support for git-pull -* https://github.com/devonfw/IDEasy/issues/1747[#1747]: Fixed macOS x64 native image build using macos-15-intel runner -* https://github.com/devonfw/IDEasy/issues/1738[#1738]: FileAccess.delete no longer follows directory links during recursive delete -* https://github.com/devonfw/IDEasy/issues/1151[#1151]: Use uname -m for runtime architecture detection on Mac/Linux -* https://github.com/devonfw/IDEasy/issues/1770[#1770]: Fix setup hanging due to buffered log output during license prompt -* https://github.com/devonfw/IDEasy/issues/1771[#1771]: Maven version 3.9.1x are now available -* https://github.com/devonfw/IDEasy/issues/1687[#1687]: Fixed JLine warning about restricted method -* https://github.com/devonfw/IDEasy/issues/1552[#1552]: Add Commandlet to fix TLS issue * https://github.com/devonfw/IDEasy/issues/1747[#1747]: macOS x64 error during installation: Bad CPU type in executable * https://github.com/devonfw/IDEasy/issues/1738[#1738]: FileAccess.delete follows links * https://github.com/devonfw/IDEasy/issues/1151[#1151]: Mac on ARM thinks the architecture is x86 and therefore does not download arm64 releases causing poor performance