From 61404934e0e717f21b8e4679fd1102c841a68f69 Mon Sep 17 00:00:00 2001 From: Jonathan Breedlove Date: Mon, 17 Feb 2025 11:17:21 -0800 Subject: [PATCH] Auto detection of proxy settings --- plugin/META-INF/MANIFEST.MF | 1 + plugin/pom.xml | 29 +-- .../fetcher/VersionManifestFetcher.java | 25 ++- .../service/DefaultTelemetryService.java | 7 +- .../eclipse/amazonq/util/ProxyUtil.java | 193 +++++++++++++----- .../eclipse/amazonq/util/ProxyUtilTest.java | 120 +++++++++-- 6 files changed, 286 insertions(+), 89 deletions(-) diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index 7155eb4e8..c8227f948 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -58,6 +58,7 @@ Bundle-Classpath: target/classes/, target/dependency/nimbus-jose-jwt-9.41.2.jar, target/dependency/profiles-2.28.26.jar, target/dependency/protocol-core-2.28.26.jar, + target/dependency/proxy-vole-1.1.6.jar, target/dependency/reactive-streams-1.0.4.jar, target/dependency/regions-2.28.26.jar, target/dependency/retries-2.28.26.jar, diff --git a/plugin/pom.xml b/plugin/pom.xml index 606bf886e..570352fa3 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -42,21 +42,21 @@ - - io.reactivex.rxjava3 - rxjava - 3.1.5 - + + io.reactivex.rxjava3 + rxjava + 3.1.5 + jakarta.inject jakarta.inject-api 2.0.1 - - io.reactivex.rxjava3 - rxjava - 3.1.5 - + + io.reactivex.rxjava3 + rxjava + 3.1.5 + com.fasterxml.jackson.core jackson-databind @@ -106,6 +106,11 @@ maven-artifact 3.9.9 + + org.bidib.com.github.markusbernhardt + proxy-vole + 1.1.6 + org.junit.jupiter junit-jupiter @@ -118,7 +123,7 @@ test - + src tst @@ -149,7 +154,7 @@ ${project.build.directory}/dependency - io.reactivex,software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven + io.reactivex,software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven,org.bidib.com.github.markusbernhardt diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java index fc546669e..81a4c7b72 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/VersionManifestFetcher.java @@ -32,6 +32,7 @@ import software.aws.toolkits.eclipse.amazonq.util.HttpClientFactory; import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; +import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification; import software.aws.toolkits.telemetry.TelemetryDefinitions.ManifestLocation; import software.aws.toolkits.telemetry.TelemetryDefinitions.Result; @@ -85,11 +86,25 @@ public Optional fetch() { return latestManifest; } catch (Exception e) { if (e.getCause() instanceof SSLHandshakeException) { - Display.getCurrent().asyncExec(() -> { - AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), - Constants.IDE_SSL_HANDSHAKE_TITLE, - Constants.IDE_SSL_HANDSHAKE_BODY); - notification.open(); + ThreadingUtils.executeAsyncTask(() -> { + Display display = null; + while (display == null) { + display = Display.getDefault(); + if (display == null) { + try { + Thread.sleep(100); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return; + } + } + } + display.asyncExec(() -> { + AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), + Constants.IDE_SSL_HANDSHAKE_TITLE, + Constants.IDE_SSL_HANDSHAKE_BODY); + notification.open(); + }); }); } Activator.getLogger().error("Error fetching manifest from remote location", e); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java index 3f1c9928d..0d813cab8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java @@ -14,6 +14,7 @@ import javax.net.ssl.SSLContext; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.SystemDefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; @@ -141,7 +142,7 @@ private static ToolkitTelemetryClient createDefaultTelemetryClient(final Region null, SSLConnectionSocketFactory.getDefaultHostnameVerifier() ); - var proxyUrl = ProxyUtil.getHttpsProxyUrl(); + var proxyUrl = ProxyUtil.getHttpsProxyUrlForEndpoint(endpoint); var httpClientBuilder = ApacheHttpClient.builder(); if (!StringUtils.isEmpty(proxyUrl)) { httpClientBuilder.proxyConfiguration(ProxyConfiguration.builder() @@ -151,7 +152,9 @@ private static ToolkitTelemetryClient createDefaultTelemetryClient(final Region httpClientBuilder.socketFactory(sslSocketFactory); - SdkHttpClient sdkHttpClient = httpClientBuilder.build(); + SdkHttpClient sdkHttpClient = httpClientBuilder + .credentialsProvider(new SystemDefaultCredentialsProvider()) + .build(); CognitoIdentityClient cognitoClient = CognitoIdentityClient.builder() .credentialsProvider(AnonymousCredentialsProvider.create()) .region(region) 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 a9b41a7ea..b6da01285 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java @@ -4,7 +4,13 @@ package software.aws.toolkits.eclipse.amazonq.util; import java.io.FileInputStream; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.ProxySelector; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.security.KeyStore; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -15,80 +21,171 @@ import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; import org.eclipse.swt.widgets.Display; +import com.github.markusbernhardt.proxy.ProxySearch; + import software.amazon.awssdk.utils.StringUtils; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; public final class ProxyUtil { + private static final String DEFAULT_PROXY_ENDPOINT = "https://amazonaws.com"; + private static volatile boolean hasSeenInvalidProxyNotification; - private static boolean hasSeenInvalidProxyNotification; + private static ProxySelector proxySelector; - private ProxyUtil() { - // Prevent initialization - } + private ProxyUtil() { } // Prevent initialization public static String getHttpsProxyUrl() { - return getHttpsProxyUrl(System.getenv("HTTPS_PROXY"), - Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.HTTPS_PROXY)); + return getHttpsProxyUrlForEndpoint(DEFAULT_PROXY_ENDPOINT); } - protected static String getHttpsProxyUrl(final String envVarValue, final String prefValue) { - String httpsProxy = envVarValue; - if (!StringUtils.isEmpty(prefValue)) { - httpsProxy = prefValue; - } + public static String getHttpsProxyUrlForEndpoint(final String endpointUrl) { try { - if (!StringUtils.isEmpty(httpsProxy)) { - URI.create(httpsProxy); - } - } catch (IllegalArgumentException e) { - if (!hasSeenInvalidProxyNotification) { - hasSeenInvalidProxyNotification = true; - Display.getDefault().asyncExec(() -> { - AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), - Constants.INVALID_PROXY_CONFIGURATION_TITLE, - Constants.INVALID_PROXY_CONFIGURATION_BODY); - notification.open(); - }); + String proxyPrefUrl = getHttpsProxyPreferenceUrl(); + if (!StringUtils.isEmpty(proxyPrefUrl)) { + return proxyPrefUrl; } + } catch (MalformedURLException e) { + showInvalidProxyNotification(); return null; } - return httpsProxy; - } - public static SSLContext getCustomSslContext() { + if (StringUtils.isEmpty(endpointUrl)) { + return null; + } + + URI endpointUri; try { - String customCertPath = System.getenv("NODE_EXTRA_CA_CERTS"); - String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT); - if (!StringUtils.isEmpty(caCertPreference)) { - customCertPath = caCertPreference; - } + endpointUri = new URI(endpointUrl); + } catch (URISyntaxException e) { + Activator.getLogger().error("Could not parse endpoint for proxy configuration: " + endpointUrl, e); + return null; + } + + return getProxyUrlFromSelector(getProxySelector(), endpointUri); + } - if (customCertPath != null && !customCertPath.isEmpty()) { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - X509Certificate cert; + private static String getProxyUrlFromSelector(final ProxySelector proxySelector, final URI endpointUri) { + if (proxySelector == null) { + return null; + } + + var proxies = proxySelector.select(endpointUri); + if (proxies == null || proxies.isEmpty()) { + return null; + } + + return proxies.stream() + .filter(p -> p.type() != Proxy.Type.DIRECT) + .findFirst() + .map(proxy -> createProxyUrl(proxy, endpointUri)) + .orElseGet(() -> { + Activator.getLogger().info("No non-DIRECT proxies found for endpoint: " + endpointUri); + return null; + }); + } - try (FileInputStream fis = new FileInputStream(customCertPath)) { - cert = (X509Certificate) certificateFactory.generateCertificate(fis); - } + private static String createProxyUrl(final Proxy proxy, final URI endpointUri) { + if (!(proxy.address() instanceof InetSocketAddress addr)) { + return null; + } - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null, null); - keyStore.setCertificateEntry("custom-cert", cert); + String scheme = determineProxyScheme(proxy.type(), endpointUri); + if (scheme == null) { + return null; + } - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keyStore); + String proxyUrl = String.format("%s://%s:%d", scheme, addr.getHostString(), addr.getPort()); + Activator.getLogger().info("Using proxy URL: " + proxyUrl + " for endpoint: " + endpointUri); + return proxyUrl; + } - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - sslContext.init(null, tmf.getTrustManagers(), null); - Activator.getLogger().info("Picked up custom CA cert."); + private static String determineProxyScheme(final Proxy.Type proxyType, final URI endpointUri) { + return switch (proxyType) { + case HTTP -> "http"; + case SOCKS -> "socks"; + default -> null; + }; + } - return sslContext; - } + protected static String getHttpsProxyPreferenceUrl() throws MalformedURLException { + String prefValue = Activator.getDefault().getPreferenceStore() + .getString(AmazonQPreferencePage.HTTPS_PROXY); + + if (StringUtils.isEmpty(prefValue)) { + return null; + } + + new URL(prefValue); // Validate URL format + return prefValue; + } + + private static void showInvalidProxyNotification() { + if (!hasSeenInvalidProxyNotification) { + hasSeenInvalidProxyNotification = true; + Display.getDefault().asyncExec(() -> { + AbstractNotificationPopup notification = new ToolkitNotification( + Display.getCurrent(), + Constants.INVALID_PROXY_CONFIGURATION_TITLE, + Constants.INVALID_PROXY_CONFIGURATION_BODY + ); + notification.open(); + }); + } + } + + public static SSLContext getCustomSslContext() { + String customCertPath = getCustomCertPath(); + if (StringUtils.isEmpty(customCertPath)) { + return null; + } + + try { + return createSslContextWithCustomCert(customCertPath); } catch (Exception e) { Activator.getLogger().error("Failed to set up SSL context. Additional certs will not be used.", e); + return null; + } + } + + private static String getCustomCertPath() { + String caCertPreference = Activator.getDefault().getPreferenceStore().getString(AmazonQPreferencePage.CA_CERT); + return !StringUtils.isEmpty(caCertPreference) ? caCertPreference : System.getenv("NODE_EXTRA_CA_CERTS"); + } + + private static SSLContext createSslContextWithCustomCert(final String certPath) throws Exception { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert; + + try (FileInputStream fis = new FileInputStream(certPath)) { + cert = (X509Certificate) certificateFactory.generateCertificate(fis); } - return null; + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setCertificateEntry("custom-cert", cert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, tmf.getTrustManagers(), null); + Activator.getLogger().info("Picked up custom CA cert."); + + return sslContext; } + static synchronized ProxySelector getProxySelector() { + if (proxySelector == null) { + ProxySearch proxySearch = new ProxySearch(); + proxySearch.addStrategy(ProxySearch.Strategy.ENV_VAR); + proxySearch.addStrategy(ProxySearch.Strategy.JAVA); + proxySearch.addStrategy(ProxySearch.Strategy.OS_DEFAULT); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + proxySearch.addStrategy(ProxySearch.Strategy.IE); + } + proxySelector = proxySearch.getProxySelector(); + } + return proxySelector; + } } 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 c9e5e983e..1525172eb 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/util/ProxyUtilTest.java @@ -3,58 +3,134 @@ package software.aws.toolkits.eclipse.amazonq.util; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.MockedStatic; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.widgets.Display; + +import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.util.Arrays; +import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; -import org.eclipse.swt.widgets.Display; +public final class ProxyUtilTest { + + @RegisterExtension + private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); -class ProxyUtilTest { + private IPreferenceStore preferenceStore; + private ProxySelector proxySelector; + + @BeforeEach + void setUp() { + preferenceStore = mock(IPreferenceStore.class); + proxySelector = mock(ProxySelector.class); + + Activator activatorMock = activatorStaticMockExtension.getMock(Activator.class); + when(activatorMock.getPreferenceStore()).thenReturn(preferenceStore); + } @Test - void testNoProxyConfigReturnsNull() { - assertEquals(null, ProxyUtil.getHttpsProxyUrl(null, null)); + void testNoProxyConfigurationReturnsNull() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(""); + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector); + when(proxySelector.select(any())).thenReturn(Collections.emptyList()); + + assertNull(ProxyUtil.getHttpsProxyUrl()); + } } @Test - void testEnvVarProxyUrl() { - String mockUrl = "http://foo.com:8888"; - assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl(mockUrl, null)); + void testPreferenceProxyUrlTakesPrecedence() { + String preferenceUrl = "http://preference.proxy:8888"; + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(preferenceUrl); + + assertEquals(preferenceUrl, ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com")); } @Test - void testPreferenceProxyUrl() { - String mockUrl = "http://foo.com:8888"; - assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl(null, mockUrl)); + void testSystemProxyConfiguration() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(""); + + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector); + + Proxy httpProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080)); + when(proxySelector.select(any())).thenReturn(Arrays.asList(httpProxy)); + + assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com")); + } } @Test - void testPreferenceProxyUrlPrecedence() { - String mockUrl = "http://foo.com:8888"; - assertEquals(mockUrl, ProxyUtil.getHttpsProxyUrl("http://bar.com:8888", mockUrl)); + void testSocksProxyConfiguration() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(""); + + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector); + + Proxy socksProxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("socks.example.com", 1080)); + when(proxySelector.select(any())).thenReturn(Arrays.asList(socksProxy)); + + assertEquals("socks://socks.example.com:1080", ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com")); + } } @Test - void testEnvVarInvalidProxyUrl() { + void testInvalidPreferenceProxyUrl() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn("invalid:url"); + try (MockedStatic displayMock = mockStatic(Display.class)) { Display mockDisplay = mock(Display.class); displayMock.when(Display::getDefault).thenReturn(mockDisplay); - String mockUrl = "127.0.0.1:8000"; - assertEquals(null, ProxyUtil.getHttpsProxyUrl(mockUrl, null)); + when(Display.getCurrent()).thenReturn(mockDisplay); + + assertNull(ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com")); } } @Test - void testPreferenceInvalidProxyUrl() { - try (MockedStatic displayMock = mockStatic(Display.class)) { - Display mockDisplay = mock(Display.class); - displayMock.when(Display::getDefault).thenReturn(mockDisplay); - String mockUrl = "127.0.0.1:8000"; - assertEquals(null, ProxyUtil.getHttpsProxyUrl(null, mockUrl)); + void testDirectProxyReturnsNull() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(""); + + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector); + + Proxy directProxy = Proxy.NO_PROXY; + when(proxySelector.select(any())).thenReturn(Arrays.asList(directProxy)); + + assertNull(ProxyUtil.getHttpsProxyUrlForEndpoint("https://foo.com")); } } + @Test + void testPreservesEndpointScheme() { + when(preferenceStore.getString(AmazonQPreferencePage.HTTPS_PROXY)).thenReturn(""); + + try (MockedStatic proxyUtilMock = mockStatic(ProxyUtil.class, CALLS_REAL_METHODS)) { + proxyUtilMock.when(ProxyUtil::getProxySelector).thenReturn(proxySelector); + + Proxy httpProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080)); + when(proxySelector.select(any())).thenReturn(Arrays.asList(httpProxy)); + + assertEquals("http://proxy.example.com:8080", ProxyUtil.getHttpsProxyUrlForEndpoint("http://foo.com")); + } + } } +