diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java index 7ede76445..496deb525 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; +import java.nio.file.Files; import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; @@ -58,13 +59,14 @@ public QLspConnectionProvider() throws IOException { @Override protected final void addEnvironmentVariables(final Map env) { String httpsProxyUrl = ProxyUtil.getHttpsProxyUrl(); - String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT); + String caCertPath = getCaCert(); + if (!StringUtils.isEmpty(httpsProxyUrl)) { env.put("HTTPS_PROXY", httpsProxyUrl); } - if (!StringUtils.isEmpty(caCertPreference)) { - env.put("NODE_EXTRA_CA_CERTS", caCertPreference); - env.put("AWS_CA_BUNDLE", caCertPreference); + if (!StringUtils.isEmpty(caCertPath)) { + env.put("NODE_EXTRA_CA_CERTS", caCertPath); + env.put("AWS_CA_BUNDLE", caCertPath); } if (ArchitectureUtils.isWindowsArm()) { env.put("DISABLE_INDEXING_LIBRARY", "true"); @@ -78,6 +80,27 @@ protected final void addEnvironmentVariables(final Map env) { } } + private String getCaCert() { + String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT); + if (!StringUtils.isEmpty(caCertPreference)) { + Activator.getLogger().info("Using user-defined CA cert: " + caCertPreference); + return caCertPreference; + } + try { + String pemContent = ProxyUtil.getCertificatesAsPem(); + if (StringUtils.isEmpty(pemContent)) { + return null; + } + var tempPath = Files.createTempFile("eclipse-q-extra-ca", ".pem"); + Activator.getLogger().info("Injecting IDE trusted certificates from " + tempPath + " into NODE_EXTRA_CA_CERTS"); + Files.write(tempPath, pemContent.getBytes()); + return tempPath.toString(); + } catch (Exception e) { + Activator.getLogger().warn("Could not create temp CA cert file", e); + return null; + } + } + private boolean needsPatchEnvVariables() { return PluginUtils.getPlatform().equals(PluginPlatform.MAC); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java index 5e419fe44..e6d6503e6 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java @@ -14,6 +14,8 @@ import java.security.KeyStore; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -190,6 +192,47 @@ private static SSLContext createSslContextWithCustomCert(final String certPath) return sslContext; } + public static String getCertificatesAsPem() { + var certs = getSystemCertificates(); + if (certs.isEmpty()) { + return null; + } + + var pemEntries = new ArrayList(); + var encoder = Base64.getMimeEncoder(64, System.lineSeparator().getBytes()); + + for (var cert : certs) { + try { + String encodedCert = encoder.encodeToString(cert.getEncoded()); + pemEntries.add("-----BEGIN CERTIFICATE-----"); + pemEntries.add(encodedCert); + pemEntries.add("-----END CERTIFICATE-----"); + } catch (Exception e) { + Activator.getLogger().error("Failed to encode certificate", e); + } + } + return String.join(System.lineSeparator(), pemEntries); + } + + public static ArrayList getSystemCertificates() { + try { + var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + var certs = new ArrayList(); + for (var tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager xtm) { + for (var cert : xtm.getAcceptedIssuers()) { + certs.add(cert); + } + } + } + return certs; + } catch (Exception e) { + Activator.getLogger().error("Failed to get system certificates", e); + return new ArrayList<>(); + } + } + static synchronized ProxySelector getProxySelector() { if (proxySelector == null) { ProxySearch proxySearch = new ProxySearch(); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProviderTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProviderTest.java index 3c7a5d5be..31983e1b0 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProviderTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProviderTest.java @@ -32,10 +32,13 @@ import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ProxyUtilsStaticMockExtension; import software.aws.toolkits.eclipse.amazonq.lsp.encryption.LspEncryptionManager; import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspInstallResult; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.LoggingService; import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; import software.aws.toolkits.eclipse.amazonq.util.ProxyUtil; +import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; +import org.eclipse.jface.preference.IPreferenceStore; public final class QLspConnectionProviderTest { @@ -54,6 +57,7 @@ public final class QLspConnectionProviderTest { private static ProxyUtilsStaticMockExtension proxyUtilsStaticMockExtension = new ProxyUtilsStaticMockExtension(); private MockedStatic pluginUtilsMock; + private IPreferenceStore preferenceStore; private static final class TestProcessConnectionProvider extends ProcessStreamConnectionProvider { @@ -79,6 +83,10 @@ public void testAddEnvironmentVariables(final Map env) { void setupMocks() { pluginUtilsMock = Mockito.mockStatic(PluginUtils.class); pluginUtilsMock.when(PluginUtils::getPlatform).thenReturn(PluginPlatform.LINUX); + + preferenceStore = Mockito.mock(IPreferenceStore.class); + var activatorMock = activatorStaticMockExtension.getMock(Activator.class); + Mockito.when(activatorMock.getPreferenceStore()).thenReturn(preferenceStore); } @AfterEach @@ -205,4 +213,45 @@ void testStartLogsErrorOnException() throws IOException { testException); } + @Test + void testCertInjectionWithUserPreference() throws IOException { + LspInstallResult lspInstallResultMock = lspManagerProviderStaticMockExtension.getMock(LspInstallResult.class); + Mockito.when(lspInstallResultMock.getServerDirectory()).thenReturn("/test/dir"); + Mockito.when(lspInstallResultMock.getServerCommand()).thenReturn("server.js"); + Mockito.when(lspInstallResultMock.getServerCommandArgs()).thenReturn(""); + + Mockito.when(preferenceStore.getString(AmazonQPreferencePage.CA_CERT)).thenReturn("/path/to/user/cert.pem"); + + MockedStatic proxyUtilStaticMock = proxyUtilsStaticMockExtension.getStaticMock(); + proxyUtilStaticMock.when(ProxyUtil::getHttpsProxyUrl).thenReturn(""); + + Map env = new HashMap<>(); + var provider = new TestQLspConnectionProvider(); + provider.testAddEnvironmentVariables(env); + + assertEquals("/path/to/user/cert.pem", env.get("NODE_EXTRA_CA_CERTS")); + assertEquals("/path/to/user/cert.pem", env.get("AWS_CA_BUNDLE")); + } + + @Test + void testNoCertInjectionWhenNoCertsFound() throws IOException { + LspInstallResult lspInstallResultMock = lspManagerProviderStaticMockExtension.getMock(LspInstallResult.class); + Mockito.when(lspInstallResultMock.getServerDirectory()).thenReturn("/test/dir"); + Mockito.when(lspInstallResultMock.getServerCommand()).thenReturn("server.js"); + Mockito.when(lspInstallResultMock.getServerCommandArgs()).thenReturn(""); + + Mockito.when(preferenceStore.getString(AmazonQPreferencePage.CA_CERT)).thenReturn(""); + + MockedStatic proxyUtilStaticMock = proxyUtilsStaticMockExtension.getStaticMock(); + proxyUtilStaticMock.when(ProxyUtil::getHttpsProxyUrl).thenReturn(""); + proxyUtilStaticMock.when(ProxyUtil::getCertificatesAsPem).thenReturn(null); + + Map env = new HashMap<>(); + var provider = new TestQLspConnectionProvider(); + provider.testAddEnvironmentVariables(env); + + assertFalse(env.containsKey("NODE_EXTRA_CA_CERTS")); + assertFalse(env.containsKey("AWS_CA_BUNDLE")); + } + } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java index 1525172eb..bc7dd8a6b 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java @@ -17,11 +17,15 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; @@ -132,5 +136,46 @@ void testPreservesEndpointScheme() { assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("http://foo.com")); } } + + @Test + void testGetCertificatesAsPemReturnsNullWhenNoCertificates() { + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(new ArrayList<>()); + + assertNull(ProxyUtil.getCertificatesAsPem()); + } + } + + @Test + void testGetCertificatesAsPemWithValidCertificates() throws Exception { + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getEncoded()).thenReturn("test-cert-data".getBytes()); + + ArrayList certs = new ArrayList<>(); + certs.add(mockCert); + proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(certs); + + String result = ProxyUtil.getCertificatesAsPem(); + assertNotNull(result); + assertTrue(result.contains("-----BEGIN CERTIFICATE-----")); + assertTrue(result.contains("-----END CERTIFICATE-----")); + } + } + + @Test + void testGetCertificatesAsPemHandlesCertificateEncodingError() throws Exception { + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getEncoded()).thenThrow(new RuntimeException("Encoding failed")); + + ArrayList certs = new ArrayList<>(); + certs.add(mockCert); + proxyUtilMock.when(ProxyUtil::getSystemCertificates).thenReturn(certs); + + String result = ProxyUtil.getCertificatesAsPem(); + assertEquals("", result); + } + } }