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

devonfw / IDEasy / 29004407614

09 Jul 2026 08:20AM UTC coverage: 72.427% (+0.2%) from 72.18%
29004407614

Pull #2138

github

web-flow
Merge 872d900bb into 8ed88cfeb
Pull Request #2138: #2137: fix fix-vpn-tls-problem not detecting TLS certificate issues behind HTTP redirects

4890 of 7468 branches covered (65.48%)

Branch coverage included in aggregate %.

12633 of 16726 relevant lines covered (75.53%)

3.19 hits per line

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

73.6
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
import java.util.Arrays;
6

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

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

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

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

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

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

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

31
  private final StringProperty url;
32

33

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

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

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

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

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

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

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

90
    String host = endpoint.host();
3✔
91
    int port = endpoint.port();
3✔
92
    String probeUrl = endpointInput.startsWith("https://") ? endpointInput : ("https://" + host + ":" + port);
7!
93
    Path customTruststorePath = this.context.getUserHomeIde().resolve("truststore").resolve("truststore.p12");
8✔
94

95
    if (TruststoreUtil.isTruststorePresent(customTruststorePath) && TruststoreUtil.isUrlReachable(probeUrl, customTruststorePath)) {
3!
96
      IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with existing custom truststore at {}.", customTruststorePath);
×
97
      configureIdeOptions(customTruststorePath);
×
98
      return;
×
99
    }
100

101
    if (TruststoreUtil.isUrlReachable(probeUrl, null)) {
4!
102
      IdeLogLevel.SUCCESS.log(LOG, "Successfully connected to {}:{} without certificate changes.", host, port);
×
103
      LOG.info("No truststore update is required for the given address.");
×
104
      if (defaultUrlUsed) {
×
105
        LOG.info(
×
106
            "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>");
107
      }
108

109
      return;
×
110
    }
111

112
    LOG.info("The given address {} is not reachable/valid without certificate changes. Continuing with certificate capture.", probeUrl);
4✔
113

114
    // download URLs are frequently redirected (e.g. https://aka.ms/...), so capture the certificate from the effective endpoint after following all redirects
115
    TruststoreUtil.TlsEndpoint effectiveEndpoint = TruststoreUtil.resolveEffectiveEndpoint(probeUrl);
3✔
116
    String effectiveHost = effectiveEndpoint.host();
3✔
117
    int effectivePort = effectiveEndpoint.port();
3✔
118

119
    X509Certificate certificate;
120
    try {
121
      certificate = TruststoreUtil.fetchServerCertificate(effectiveHost, effectivePort);
4✔
122
    } catch (Exception e) {
1✔
123
      LOG.error("Failed to capture certificate from {}:{}.", effectiveHost, effectivePort, e);
18✔
124
      IdeLogLevel.INTERACTION.log(LOG,
4✔
125
          "Please check proxy/VPN and retry. You can also follow: https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc#tls-certificate-issues");
126
      return;
1✔
127
    }
1✔
128

129
    LOG.info("Captured untrusted certificate from {}:{}:", effectiveHost, effectivePort);
6✔
130
    LOG.info(TruststoreUtil.describeCertificate(certificate));
4✔
131

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

134
    if (!addToTruststore) {
2!
135
      LOG.info("Skipped truststore update by user choice.");
×
136
      return;
×
137
    }
138

139
    try {
140
      TruststoreUtil.createOrUpdateTruststore(customTruststorePath, certificate, "custom");
4✔
141
      IdeLogLevel.SUCCESS.log(LOG, "Custom truststore updated at {}", customTruststorePath);
10✔
142
    } catch (Exception e) {
×
143
      LOG.error("Failed to create or update custom truststore at {}", customTruststorePath, e);
×
144
      return;
×
145
    }
1✔
146

147
    configureIdeOptions(customTruststorePath);
3✔
148

149
    if (TruststoreUtil.isUrlReachable(probeUrl, customTruststorePath)) {
4!
150
      IdeLogLevel.SUCCESS.log(LOG, "TLS handshake succeeded with custom truststore.");
5✔
151
    } else {
152
      LOG.warn("TLS handshake still fails even with custom truststore.");
×
153
    }
154
  }
1✔
155

156
  private void configureIdeOptions(Path customTruststorePath) {
157
    String truststorePath = customTruststorePath.toAbsolutePath().toString();
4✔
158
    String truststoreOption = TRUSTSTORE_OPTION_PREFIX + truststorePath;
3✔
159
    String truststorePasswordOption = TRUSTSTORE_PASSWORD_OPTION_PREFIX + TruststoreUtil.CUSTOM_TRUSTSTORE_PASSWORD;
2✔
160

161
    EnvironmentVariables confVariables = this.context.getVariables().getByType(EnvironmentVariablesType.USER);
6✔
162

163
    if (confVariables == null) {
2!
164
      IdeLogLevel.INTERACTION.log(LOG, "Please configure IDE_OPTIONS manually: {} {}", truststoreOption, truststorePasswordOption);
×
165
      return;
×
166
    }
167

168
    String options = confVariables.getFlat(IDE_OPTIONS);
4✔
169
    options = removeOptionWithPrefix(options, TRUSTSTORE_OPTION_PREFIX);
4✔
170
    options = removeOptionWithPrefix(options, TRUSTSTORE_PASSWORD_OPTION_PREFIX);
4✔
171
    options = appendOption(options, truststoreOption);
4✔
172
    options = appendOption(options, truststorePasswordOption);
4✔
173

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

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

203
  private static String appendOption(String options, String option) {
204
    if ((options == null) || options.isBlank()) {
5!
205
      return option;
2✔
206
    }
207
    return options + " " + option;
4✔
208
  }
209
}
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