Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto detection of proxy settings #372

Merged
merged 2 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugin/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 17 additions & 12 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,21 @@
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down Expand Up @@ -106,6 +106,11 @@
<artifactId>maven-artifact</artifactId>
<version>3.9.9</version>
</dependency>
<dependency>
<groupId>org.bidib.com.github.markusbernhardt</groupId>
<artifactId>proxy-vole</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -118,7 +123,7 @@
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>tst</testSourceDirectory>
Expand Down Expand Up @@ -149,7 +154,7 @@
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependency</outputDirectory>
<includeGroupIds>io.reactivex,software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven</includeGroupIds>
<includeGroupIds>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</includeGroupIds>
</configuration>
</execution>
<execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,11 +86,25 @@ public Optional<Manifest> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
193 changes: 145 additions & 48 deletions plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does making this an Optional make sense? It would make it explicit to the user that the String could be null

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;
}
}
Loading
Loading