Skip to content

Commit bd51f6c

Browse files
authored
Pass IDE trusted system certs to Language server by default (#482)
This change improves the proxy support story for the extension. With this, we honor customer CA cert if specified in the preferences UI. If it is not supplied, instead of leaving it blank, we now detect system certificates and send it over to the node based language server. This allows us to address some issues where users are on corporate proxies/firewalls that have a proxy url but not an explicitly defined cert and expects applications to honor system certs. We currently do the same system cert detection when downloading artifacts for lsp. Follows a similar approach as JB: aws/aws-toolkit-jetbrains#5553
1 parent 30d07b3 commit bd51f6c

File tree

4 files changed

+164
-4
lines changed

4 files changed

+164
-4
lines changed

plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.io.IOException;
88
import java.io.InputStreamReader;
99
import java.io.OutputStream;
10+
import java.nio.file.Files;
1011
import java.nio.file.Paths;
1112
import java.time.Instant;
1213
import java.util.ArrayList;
@@ -58,13 +59,14 @@ public QLspConnectionProvider() throws IOException {
5859
@Override
5960
protected final void addEnvironmentVariables(final Map<String, String> env) {
6061
String httpsProxyUrl = ProxyUtil.getHttpsProxyUrl();
61-
String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT);
62+
String caCertPath = getCaCert();
63+
6264
if (!StringUtils.isEmpty(httpsProxyUrl)) {
6365
env.put("HTTPS_PROXY", httpsProxyUrl);
6466
}
65-
if (!StringUtils.isEmpty(caCertPreference)) {
66-
env.put("NODE_EXTRA_CA_CERTS", caCertPreference);
67-
env.put("AWS_CA_BUNDLE", caCertPreference);
67+
if (!StringUtils.isEmpty(caCertPath)) {
68+
env.put("NODE_EXTRA_CA_CERTS", caCertPath);
69+
env.put("AWS_CA_BUNDLE", caCertPath);
6870
}
6971
if (ArchitectureUtils.isWindowsArm()) {
7072
env.put("DISABLE_INDEXING_LIBRARY", "true");
@@ -78,6 +80,27 @@ protected final void addEnvironmentVariables(final Map<String, String> env) {
7880
}
7981
}
8082

83+
private String getCaCert() {
84+
String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT);
85+
if (!StringUtils.isEmpty(caCertPreference)) {
86+
Activator.getLogger().info("Using user-defined CA cert: " + caCertPreference);
87+
return caCertPreference;
88+
}
89+
try {
90+
String pemContent = ProxyUtil.getCertificatesAsPem();
91+
if (StringUtils.isEmpty(pemContent)) {
92+
return null;
93+
}
94+
var tempPath = Files.createTempFile("eclipse-q-extra-ca", ".pem");
95+
Activator.getLogger().info("Injecting IDE trusted certificates from " + tempPath + " into NODE_EXTRA_CA_CERTS");
96+
Files.write(tempPath, pemContent.getBytes());
97+
return tempPath.toString();
98+
} catch (Exception e) {
99+
Activator.getLogger().warn("Could not create temp CA cert file", e);
100+
return null;
101+
}
102+
}
103+
81104
private boolean needsPatchEnvVariables() {
82105
return PluginUtils.getPlatform().equals(PluginPlatform.MAC);
83106
}

plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.security.KeyStore;
1515
import java.security.cert.CertificateFactory;
1616
import java.security.cert.X509Certificate;
17+
import java.util.ArrayList;
18+
import java.util.Base64;
1719

1820
import javax.net.ssl.SSLContext;
1921
import javax.net.ssl.TrustManager;
@@ -190,6 +192,47 @@ private static SSLContext createSslContextWithCustomCert(final String certPath)
190192
return sslContext;
191193
}
192194

195+
public static String getCertificatesAsPem() {
196+
var certs = getSystemCertificates();
197+
if (certs.isEmpty()) {
198+
return null;
199+
}
200+
201+
var pemEntries = new ArrayList<String>();
202+
var encoder = Base64.getMimeEncoder(64, System.lineSeparator().getBytes());
203+
204+
for (var cert : certs) {
205+
try {
206+
String encodedCert = encoder.encodeToString(cert.getEncoded());
207+
pemEntries.add("-----BEGIN CERTIFICATE-----");
208+
pemEntries.add(encodedCert);
209+
pemEntries.add("-----END CERTIFICATE-----");
210+
} catch (Exception e) {
211+
Activator.getLogger().error("Failed to encode certificate", e);
212+
}
213+
}
214+
return String.join(System.lineSeparator(), pemEntries);
215+
}
216+
217+
public static ArrayList<X509Certificate> getSystemCertificates() {
218+
try {
219+
var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
220+
tmf.init((KeyStore) null);
221+
var certs = new ArrayList<X509Certificate>();
222+
for (var tm : tmf.getTrustManagers()) {
223+
if (tm instanceof X509TrustManager xtm) {
224+
for (var cert : xtm.getAcceptedIssuers()) {
225+
certs.add(cert);
226+
}
227+
}
228+
}
229+
return certs;
230+
} catch (Exception e) {
231+
Activator.getLogger().error("Failed to get system certificates", e);
232+
return new ArrayList<>();
233+
}
234+
}
235+
193236
static synchronized ProxySelector getProxySelector() {
194237
if (proxySelector == null) {
195238
ProxySearch proxySearch = new ProxySearch();

plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProviderTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ProxyUtilsStaticMockExtension;
3333
import software.aws.toolkits.eclipse.amazonq.lsp.encryption.LspEncryptionManager;
3434
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspInstallResult;
35+
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
3536
import software.aws.toolkits.eclipse.amazonq.util.LoggingService;
3637
import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform;
3738
import software.aws.toolkits.eclipse.amazonq.util.PluginUtils;
3839
import software.aws.toolkits.eclipse.amazonq.util.ProxyUtil;
40+
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;
41+
import org.eclipse.jface.preference.IPreferenceStore;
3942

4043
public final class QLspConnectionProviderTest {
4144

@@ -54,6 +57,7 @@ public final class QLspConnectionProviderTest {
5457
private static ProxyUtilsStaticMockExtension proxyUtilsStaticMockExtension = new ProxyUtilsStaticMockExtension();
5558

5659
private MockedStatic<PluginUtils> pluginUtilsMock;
60+
private IPreferenceStore preferenceStore;
5761

5862
private static final class TestProcessConnectionProvider extends ProcessStreamConnectionProvider {
5963

@@ -79,6 +83,10 @@ public void testAddEnvironmentVariables(final Map<String, String> env) {
7983
void setupMocks() {
8084
pluginUtilsMock = Mockito.mockStatic(PluginUtils.class);
8185
pluginUtilsMock.when(PluginUtils::getPlatform).thenReturn(PluginPlatform.LINUX);
86+
87+
preferenceStore = Mockito.mock(IPreferenceStore.class);
88+
var activatorMock = activatorStaticMockExtension.getMock(Activator.class);
89+
Mockito.when(activatorMock.getPreferenceStore()).thenReturn(preferenceStore);
8290
}
8391

8492
@AfterEach
@@ -205,4 +213,45 @@ void testStartLogsErrorOnException() throws IOException {
205213
testException);
206214
}
207215

216+
@Test
217+
void testCertInjectionWithUserPreference() throws IOException {
218+
LspInstallResult lspInstallResultMock = lspManagerProviderStaticMockExtension.getMock(LspInstallResult.class);
219+
Mockito.when(lspInstallResultMock.getServerDirectory()).thenReturn("/test/dir");
220+
Mockito.when(lspInstallResultMock.getServerCommand()).thenReturn("server.js");
221+
Mockito.when(lspInstallResultMock.getServerCommandArgs()).thenReturn("");
222+
223+
Mockito.when(preferenceStore.getString(AmazonQPreferencePage.CA_CERT)).thenReturn("/path/to/user/cert.pem");
224+
225+
MockedStatic<ProxyUtil> proxyUtilStaticMock = proxyUtilsStaticMockExtension.getStaticMock();
226+
proxyUtilStaticMock.when(ProxyUtil::getHttpsProxyUrl).thenReturn("");
227+
228+
Map<String, String> env = new HashMap<>();
229+
var provider = new TestQLspConnectionProvider();
230+
provider.testAddEnvironmentVariables(env);
231+
232+
assertEquals("/path/to/user/cert.pem", env.get("NODE_EXTRA_CA_CERTS"));
233+
assertEquals("/path/to/user/cert.pem", env.get("AWS_CA_BUNDLE"));
234+
}
235+
236+
@Test
237+
void testNoCertInjectionWhenNoCertsFound() throws IOException {
238+
LspInstallResult lspInstallResultMock = lspManagerProviderStaticMockExtension.getMock(LspInstallResult.class);
239+
Mockito.when(lspInstallResultMock.getServerDirectory()).thenReturn("/test/dir");
240+
Mockito.when(lspInstallResultMock.getServerCommand()).thenReturn("server.js");
241+
Mockito.when(lspInstallResultMock.getServerCommandArgs()).thenReturn("");
242+
243+
Mockito.when(preferenceStore.getString(AmazonQPreferencePage.CA_CERT)).thenReturn("");
244+
245+
MockedStatic<ProxyUtil> proxyUtilStaticMock = proxyUtilsStaticMockExtension.getStaticMock();
246+
proxyUtilStaticMock.when(ProxyUtil::getHttpsProxyUrl).thenReturn("");
247+
proxyUtilStaticMock.when(ProxyUtil::getCertificatesAsPem).thenReturn(null);
248+
249+
Map<String, String> env = new HashMap<>();
250+
var provider = new TestQLspConnectionProvider();
251+
provider.testAddEnvironmentVariables(env);
252+
253+
assertFalse(env.containsKey("NODE_EXTRA_CA_CERTS"));
254+
assertFalse(env.containsKey("AWS_CA_BUNDLE"));
255+
}
256+
208257
}

plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
import java.net.InetSocketAddress;
1818
import java.net.Proxy;
1919
import java.net.ProxySelector;
20+
import java.security.cert.X509Certificate;
21+
import java.util.ArrayList;
2022
import java.util.Arrays;
2123
import java.util.Collections;
2224

2325
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2427
import static org.junit.jupiter.api.Assertions.assertNull;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
2529
import static org.mockito.ArgumentMatchers.any;
2630
import static org.mockito.Mockito.CALLS_REAL_METHODS;
2731
import static org.mockito.Mockito.mock;
@@ -132,5 +136,46 @@ void testPreservesEndpointScheme() {
132136
assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("http://foo.com"));
133137
}
134138
}
139+
140+
@Test
141+
void testGetCertificatesAsPemReturnsNullWhenNoCertificates() {
142+
try (MockedStatic<ProxyUtil> proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
143+
proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(new ArrayList<>());
144+
145+
assertNull(ProxyUtil.getCertificatesAsPem());
146+
}
147+
}
148+
149+
@Test
150+
void testGetCertificatesAsPemWithValidCertificates() throws Exception {
151+
try (MockedStatic<ProxyUtil> proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
152+
X509Certificate mockCert = mock(X509Certificate.class);
153+
when(mockCert.getEncoded()).thenReturn("test-cert-data".getBytes());
154+
155+
ArrayList<X509Certificate> certs = new ArrayList<>();
156+
certs.add(mockCert);
157+
proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(certs);
158+
159+
String result = ProxyUtil.getCertificatesAsPem();
160+
assertNotNull(result);
161+
assertTrue(result.contains("-----BEGIN CERTIFICATE-----"));
162+
assertTrue(result.contains("-----END CERTIFICATE-----"));
163+
}
164+
}
165+
166+
@Test
167+
void testGetCertificatesAsPemHandlesCertificateEncodingError() throws Exception {
168+
try (MockedStatic<ProxyUtil> proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) {
169+
X509Certificate mockCert = mock(X509Certificate.class);
170+
when(mockCert.getEncoded()).thenThrow(new RuntimeException("Encoding failed"));
171+
172+
ArrayList<X509Certificate> certs = new ArrayList<>();
173+
certs.add(mockCert);
174+
proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(certs);
175+
176+
String result = ProxyUtil.getCertificatesAsPem();
177+
assertEquals("", result);
178+
}
179+
}
135180
}
136181

0 commit comments

Comments
 (0)