• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 29010809949

09 Jul 2026 10:12AM UTC coverage: 72.484% (+0.3%) from 72.201%
29010809949

Pull #2138

github

web-flow
Merge a29ca6e33 into c56a44423
Pull Request #2138: #2137: fix fix-vpn-tls-problem not detecting TLS certificate issues behind HTTP redirects

4898 of 7470 branches covered (65.57%)

Branch coverage included in aggregate %.

12641 of 16727 relevant lines covered (75.57%)

3.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.88
cli/src/main/java/com/devonfw/tools/ide/commandlet/TruststoreCommandlet.java
1
package com.devonfw.tools.ide.commandlet;
2

3
import java.nio.file.Path;
4
import java.security.cert.X509Certificate;
5

6
import org.slf4j.Logger;
7
import org.slf4j.LoggerFactory;
8

9
import com.devonfw.tools.ide.cli.CliException;
10
import com.devonfw.tools.ide.context.IdeContext;
11
import com.devonfw.tools.ide.environment.EnvironmentVariables;
12
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
13
import com.devonfw.tools.ide.log.IdeLogLevel;
14
import com.devonfw.tools.ide.property.StringProperty;
15
import com.devonfw.tools.ide.util.TruststoreUtil;
16

17
/**
18
 * {@link Commandlet} to fix the TLS problem for VPN users.
19
 */
20
public class TruststoreCommandlet extends Commandlet {
21

22
  private static final Logger LOG = LoggerFactory.getLogger(TruststoreCommandlet.class);
4✔
23

24
  private static final String IDE_OPTIONS = "IDE_OPTIONS";
25

26
  private static final String TRUSTSTORE_OPTION_PREFIX = "-Djavax.net.ssl.trustStore=";
27

28
  private static final String TRUSTSTORE_PASSWORD_OPTION_PREFIX = "-Djavax.net.ssl.trustStorePassword=";
29

30
  private final StringProperty url;
31

32

33
  /**
34
   * The constructor.
35
   *
36
   * @param context the {@link IdeContext}.
37
   */
38
  public TruststoreCommandlet(IdeContext context) {
39
    super(context);
3✔
40
    addKeyword(getName());
4✔
41
    this.url = add(new StringProperty("", false, "url"));
11✔
42
  }
1✔
43

44
  @Override
45
  public String getName() {
46
    return "fix-vpn-tls-problem";
2✔
47
  }
48

49
  @Override
50
  public boolean isIdeHomeRequired() {
51
    return false;
2✔
52
  }
53

54
  /**
55
   * 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
56
   * 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
57
   * is already reachable or if the certificate is already trusted.
58
   * <p>
59
   * The flow is as follows:
60
   * <ul>
61
   * <li>Parse the input URL/host and port.</li>
62
   * <li>Check if a custom truststore already exists and can establish a TLS connection to the endpoint. If yes, exit successfully.</li>
63
   * <li>Check if the endpoint is reachable without any certificate changes. If yes, exit successfully.</li>
64
   * <li>Try to capture the server certificate from the endpoint. If it fails, log an error and exit.</li>
65
   * <li>Show the captured certificate details to the user and ask if they want to add it to the custom truststore.</li>
66
   * <li>If the user agrees, ask for a password for the custom truststore and create/update it with the captured certificate.</li>
67
   * <li>Configure IDE_OPTIONS to use the custom truststore by default.</li>
68
   * <li>Check if the endpoint is now reachable with the custom truststore and log the result.</li>
69
   * </ul>
70
   */
71
  @Override
72
  protected void doRun() {
73

74
    String endpointInput = this.url.getValueAsString();
4✔
75
    boolean defaultUrlUsed = false;
2✔
76

77
    if (endpointInput == null || endpointInput.isBlank()) {
5!
78
      endpointInput = "https://www.github.com";
×
79
      defaultUrlUsed = true;
×
80
    }
81

82
    TruststoreUtil.TlsEndpoint endpoint;
83
    try {
84
      endpoint = TruststoreUtil.parseTlsEndpoint(endpointInput);
3✔
85
    } catch (IllegalArgumentException e) {
1✔
86
      throw new CliException("Invalid target URL/host '" + endpointInput + "': " + e.getMessage(), e);
9✔
87
    }
1✔
88

89
    Path customTruststorePath = this.context.getUserHomeIde().resolve("truststore").resolve("truststore.p12");
8✔
90

91
    if (TruststoreUtil.isTruststorePresent(customTruststorePath) && TruststoreUtil.isReachable(endpoint, customTruststorePath)) {
3!
92
      IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath);
×
93
      configureIdeOptions(customTruststorePath);
×
94
      return;
×
95
    }
96

97
    if (TruststoreUtil.isReachable(endpoint, null)) {
4!
98
      IdeLogLevel.SUCCESS.log(LOG, "Successfully connected to {}:{} without certificate changes.", endpoint.host(), endpoint.port());
×
99
      LOG.info("No truststore update is required for the given address.");
×
100
      if (defaultUrlUsed) {
×
101
        LOG.info(
×
102
            "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 <url>");
103
      }
104

105
      return;
×
106
    }
107

108
    LOG.info("The given address {} is not reachable/valid without certificate changes. Continuing with certificate capture.", endpoint.url());
5✔
109

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

113
    X509Certificate certificate;
114
    try {
115
      certificate = TruststoreUtil.fetchServerCertificate(effectiveEndpoint.host(), effectiveEndpoint.port());
6✔
116
    } catch (Exception e) {
1✔
117
      LOG.error("Failed to capture certificate from {}:{}.", effectiveEndpoint.host(), effectiveEndpoint.port(), e);
20✔
118
      IdeLogLevel.INTERACTION.log(LOG,
4✔
119
          "Please check proxy/VPN and retry. You can also follow: https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues");
120
      return;
1✔
121
    }
1✔
122

123
    LOG.info("Captured untrusted certificate from {}:{}:", effectiveEndpoint.host(), effectiveEndpoint.port());
8✔
124
    LOG.info(TruststoreUtil.describeCertificate(certificate));
4✔
125

126
    boolean addToTruststore = this.context.question("Do you want to add this certificate to the custom truststore at {}?", customTruststorePath);
11✔
127

128
    if (!addToTruststore) {
2!
129
      LOG.info("Skipped truststore update by user choice.");
×
130
      return;
×
131
    }
132

133
    try {
134
      TruststoreUtil.createOrUpdateTruststore(customTruststorePath, certificate, "custom");
4✔
135
      IdeLogLevel.SUCCESS.log(LOG, "Custom truststore updated at {}", customTruststorePath);
10✔
136
    } catch (Exception e) {
×
137
      LOG.error("Failed to create or update custom truststore at {}", customTruststorePath, e);
×
138
      return;
×
139
    }
1✔
140

141
    configureIdeOptions(customTruststorePath);
3✔
142

143
    if (TruststoreUtil.isReachable(endpoint, customTruststorePath)) {
4!
144
      IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with custom truststore.");
5✔
145
    } else {
146
      LOG.warn("TLS handshake still fails even with custom truststore.");
×
147
    }
148
  }
1✔
149

150
  private void configureIdeOptions(Path customTruststorePath) {
151
    String truststorePath = customTruststorePath.toAbsolutePath().toString();
4✔
152
    String truststoreOption = TRUSTSTORE_OPTION_PREFIX + truststorePath;
3✔
153
    String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD;
2✔
154

155
    EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.USER);
6✔
156

157
    if (confVariables == null) {
2!
158
      IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption);
×
159
      return;
×
160
    }
161

162
    String options = confVariables.getFlat(IDE_OPTIONS);
4✔
163
    options = removeOptionWithPrefix(options, TRUSTSTORE_OPTION_PREFIX);
4✔
164
    options = removeOptionWithPrefix(options, TRUSTSTORE_PASSWORD_OPTION_PREFIX);
4✔
165
    options = appendOption(options, truststoreOption);
4✔
166
    options = appendOption(options, truststorePasswordOption);
4✔
167

168
    try {
169
      confVariables.set(IDE_OPTIONS, options, true);
6✔
170
      confVariables.save();
2✔
171
      // Apply directly for the current process as well.
172
      System.setProperty("javax.net.ssl.trustStore", truststorePath);
4✔
173
      System.setProperty("javax.net.ssl.trustStorePassword", TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD);
4✔
174
      IdeLogLevel.SUCCESS.log(LOG, "IDE_OPTIONS configured to use custom truststore by default.");
4✔
175
    } catch (UnsupportedOperationException e) {
×
176
      IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption);
×
177
    }
1✔
178
  }
1✔
179

180
  private static String removeOptionWithPrefix(String options, String prefix) {
181
    if ((options == null) || options.isBlank()) {
5✔
182
      return "";
2✔
183
    }
184
    StringBuilder result = new StringBuilder();
4✔
185
    String[] tokens = options.trim().split("\\s+");
5✔
186
    for (String token : tokens) {
16✔
187
      if (!token.startsWith(prefix)) {
4✔
188
        if (!result.isEmpty()) {
3✔
189
          result.append(' ');
4✔
190
        }
191
        result.append(token);
4✔
192
      }
193
    }
194
    return result.toString();
3✔
195
  }
196

197
  private static String appendOption(String options, String option) {
198
    if ((options == null) || options.isBlank()) {
5!
199
      return option;
2✔
200
    }
201
    return options + " " + option;
4✔
202
  }
203
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc