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"));
+ }
+ }
}
+