From bbb988affaad77c1080c09952c99ea3ff1f4e1f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 23:54:06 +0000 Subject: [PATCH 1/2] Support HTTP_PROXY/HTTPS_PROXY environment variables for proxy configuration Maven previously ignored standard Unix proxy environment variables, requiring users to configure proxies exclusively via ~/.m2/settings.xml. This breaks CI/CD pipelines and environments where proxies are set via env vars (e.g. HTTP_PROXY, HTTPS_PROXY, NO_PROXY). This change reads these env vars from the merged properties map (where they are already available with env. prefix via EnvironmentUtils) and adds them to the Aether DefaultProxySelector as a fallback when no settings.xml proxy covers a given protocol. Settings.xml proxies always take precedence. Supported env vars (lowercase preferred over uppercase, Unix curl convention): - HTTP_PROXY / http_proxy -> proxy for http:// repositories - HTTPS_PROXY / https_proxy -> proxy for https:// repositories - ALL_PROXY / all_proxy -> fallback for both protocols - NO_PROXY / no_proxy -> comma-separated exclusion list https://claude.ai/code/session_01Fsmh9ieCCeuLUt45Fn7scs --- ...DefaultRepositorySystemSessionFactory.java | 73 ++++++++++++++++ ...ultRepositorySystemSessionFactoryTest.java | 83 +++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java index 541c8364ed8e..b0c7d420811d 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java @@ -18,6 +18,8 @@ */ package org.apache.maven.internal.aether; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -27,6 +29,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -212,6 +215,7 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) sessionBuilder.setMirrorSelector(mirrorSelector); DefaultProxySelector proxySelector = new DefaultProxySelector(); + Set coveredProtocols = new HashSet<>(); for (Proxy proxy : request.getProxies()) { AuthenticationBuilder authBuilder = new AuthenticationBuilder(); authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword()); @@ -219,7 +223,9 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) new org.eclipse.aether.repository.Proxy( proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()), proxy.getNonProxyHosts()); + coveredProtocols.add(proxy.getProtocol()); } + addProxiesFromEnvironment(proxySelector, coveredProtocols, mergedProps); sessionBuilder.setProxySelector(proxySelector); // Note: we do NOT use WagonTransportConfigurationKeys here as Maven Core does NOT depend on Wagon Transport @@ -509,6 +515,73 @@ private Map getPropertiesFromRequestedProfiles(MavenExecutionReq e -> String.valueOf(e.getKey()), e -> String.valueOf(e.getValue()), (k1, k2) -> k2)); } + private void addProxiesFromEnvironment( + DefaultProxySelector proxySelector, Set coveredProtocols, Map mergedProps) { + // Convert NO_PROXY comma-separated list to Maven's pipe-separated nonProxyHosts + String noProxy = getEnvVar(mergedProps, "NO_PROXY", "no_proxy"); + String nonProxyHosts = noProxy != null ? noProxy.replace(",", "|") : null; + + if (!coveredProtocols.contains("http")) { + String httpProxy = getEnvVar(mergedProps, "HTTP_PROXY", "http_proxy"); + if (httpProxy == null) { + httpProxy = getEnvVar(mergedProps, "ALL_PROXY", "all_proxy"); + } + if (httpProxy != null) { + addProxyFromUrl(proxySelector, "http", httpProxy, nonProxyHosts); + } + } + + if (!coveredProtocols.contains("https")) { + String httpsProxy = getEnvVar(mergedProps, "HTTPS_PROXY", "https_proxy"); + if (httpsProxy == null) { + httpsProxy = getEnvVar(mergedProps, "ALL_PROXY", "all_proxy"); + } + if (httpsProxy != null) { + addProxyFromUrl(proxySelector, "https", httpsProxy, nonProxyHosts); + } + } + } + + /** Returns env var value: prefers lowercase key (Unix curl convention), falls back to uppercase. */ + private String getEnvVar(Map mergedProps, String upperCase, String lowerCase) { + String value = mergedProps.get("env." + lowerCase); + if (value == null) { + value = mergedProps.get("env." + upperCase); + } + return value; + } + + private void addProxyFromUrl( + DefaultProxySelector proxySelector, String protocol, String proxyUrl, String nonProxyHosts) { + try { + URI uri = new URI(proxyUrl); + String host = uri.getHost(); + if (host == null || host.isEmpty()) { + logger.warn("Ignoring invalid proxy URL from environment variable: {}", proxyUrl); + return; + } + int port = uri.getPort(); + if (port < 0) { + port = "https".equals(protocol) ? 443 : 80; + } + AuthenticationBuilder authBuilder = new AuthenticationBuilder(); + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + int colonIdx = userInfo.indexOf(':'); + if (colonIdx >= 0) { + authBuilder.addUsername(userInfo.substring(0, colonIdx)); + authBuilder.addPassword(userInfo.substring(colonIdx + 1)); + } else { + authBuilder.addUsername(userInfo); + } + } + proxySelector.add( + new org.eclipse.aether.repository.Proxy(protocol, host, port, authBuilder.build()), nonProxyHosts); + } catch (URISyntaxException e) { + logger.warn("Failed to parse proxy URL from environment variable: {}", proxyUrl, e); + } + } + private String getUserAgent() { String version = runtimeInformation.getMavenVersion(); version = version.isEmpty() ? version : "/" + version; diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java index 8b80dc0efd60..e73cf68dc703 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java @@ -40,7 +40,9 @@ import org.codehaus.plexus.testing.PlexusTest; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.collection.VersionFilter; +import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.util.graph.version.ChainedVersionFilter; import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter; @@ -448,6 +450,87 @@ void versionFilteringTest() throws InvalidRepositoryException { assertInstanceOf(ChainedVersionFilter.class, versionFilter); } + @Test + void proxyFromEnvVarsTest() throws InvalidRepositoryException { + DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( + aetherRepositorySystem, + eventSpyDispatcher, + information, + defaultTypeRegistry, + versionScheme, + Collections.emptyMap()); + + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setLocalRepository(getLocalRepository()); + + Properties systemProps = new Properties(); + systemProps.setProperty("env.HTTP_PROXY", "http://proxy.example.com:3128"); + systemProps.setProperty("env.HTTPS_PROXY", "http://proxy.example.com:3128"); + systemProps.setProperty("env.NO_PROXY", "localhost,127.0.0.1"); + request.setSystemProperties(systemProps); + + RepositorySystemSession session = systemSessionFactory.newRepositorySession(request); + + RemoteRepository httpRepo = new RemoteRepository.Builder("test", "default", "http://repo.example.com/").build(); + org.eclipse.aether.repository.Proxy httpProxy = + session.getProxySelector().getProxy(httpRepo); + assertNotNull(httpProxy); + assertEquals("proxy.example.com", httpProxy.getHost()); + assertEquals(3128, httpProxy.getPort()); + + RemoteRepository httpsRepo = + new RemoteRepository.Builder("test2", "default", "https://repo.example.com/").build(); + org.eclipse.aether.repository.Proxy httpsProxy = + session.getProxySelector().getProxy(httpsRepo); + assertNotNull(httpsProxy); + assertEquals("proxy.example.com", httpsProxy.getHost()); + assertEquals(3128, httpsProxy.getPort()); + } + + @Test + void settingsProxyTakesPrecedenceOverEnvVarsTest() throws InvalidRepositoryException { + DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( + aetherRepositorySystem, + eventSpyDispatcher, + information, + defaultTypeRegistry, + versionScheme, + Collections.emptyMap()); + + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setLocalRepository(getLocalRepository()); + + // Settings proxy for http + org.apache.maven.settings.Proxy settingsProxy = new org.apache.maven.settings.Proxy(); + settingsProxy.setProtocol("http"); + settingsProxy.setHost("settings-proxy.example.com"); + settingsProxy.setPort(8080); + request.addProxy(settingsProxy); + + // Env var also set (should be ignored for http, used for https) + Properties systemProps = new Properties(); + systemProps.setProperty("env.HTTP_PROXY", "http://env-proxy.example.com:3128"); + systemProps.setProperty("env.HTTPS_PROXY", "http://env-proxy.example.com:3128"); + request.setSystemProperties(systemProps); + + RepositorySystemSession session = systemSessionFactory.newRepositorySession(request); + + // Settings proxy wins for http + RemoteRepository httpRepo = new RemoteRepository.Builder("test", "default", "http://repo.example.com/").build(); + org.eclipse.aether.repository.Proxy httpProxy = + session.getProxySelector().getProxy(httpRepo); + assertNotNull(httpProxy); + assertEquals("settings-proxy.example.com", httpProxy.getHost()); + + // Env var used for https (not covered by settings) + RemoteRepository httpsRepo = + new RemoteRepository.Builder("test2", "default", "https://repo.example.com/").build(); + org.eclipse.aether.repository.Proxy httpsProxy = + session.getProxySelector().getProxy(httpsRepo); + assertNotNull(httpsProxy); + assertEquals("env-proxy.example.com", httpsProxy.getHost()); + } + protected ArtifactRepository getLocalRepository() throws InvalidRepositoryException { File repoDir = new File(getBasedir(), "target/local-repo").getAbsoluteFile(); From 8f87eafe50f9cded69edc41ae004784ed299937c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 03:23:11 +0000 Subject: [PATCH 2/2] Add e2e integration test for proxy env var via EnvironmentUtils Adds proxyFromEnvVarsViaEnvironmentUtilsTest which exercises the full chain: real System.getenv(HTTPS_PROXY) -> EnvironmentUtils.addEnvVars() -> createMergedProperties() -> addProxiesFromEnvironment() -> Aether proxy selector, mirroring the actual CLI code path in BaseParser. The test skips automatically when HTTPS_PROXY is not set in the environment, so it runs as an optional e2e test in environments that have a proxy configured. https://claude.ai/code/session_01Fsmh9ieCCeuLUt45Fn7scs --- ...ultRepositorySystemSessionFactoryTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java index e73cf68dc703..d0b1ac20d5bf 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java @@ -34,6 +34,7 @@ import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.internal.impl.DefaultTypeRegistry; +import org.apache.maven.properties.internal.EnvironmentUtils; import org.apache.maven.rtinfo.RuntimeInformation; import org.apache.maven.settings.Server; import org.codehaus.plexus.configuration.PlexusConfiguration; @@ -50,6 +51,7 @@ import org.eclipse.aether.util.graph.version.LowestVersionFilter; import org.eclipse.aether.util.graph.version.PredicateVersionFilter; import org.eclipse.aether.version.VersionScheme; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import static org.codehaus.plexus.testing.PlexusExtension.getBasedir; @@ -531,6 +533,39 @@ void settingsProxyTakesPrecedenceOverEnvVarsTest() throws InvalidRepositoryExcep assertEquals("env-proxy.example.com", httpsProxy.getHost()); } + @Test + void proxyFromEnvVarsViaEnvironmentUtilsTest() throws InvalidRepositoryException { + // Skip if HTTPS_PROXY is not set in the real environment + String httpsProxy = System.getenv("HTTPS_PROXY"); + if (httpsProxy == null) { + httpsProxy = System.getenv("https_proxy"); + } + Assumptions.assumeTrue(httpsProxy != null, "HTTPS_PROXY env var not set; skipping e2e env var test"); + + DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( + aetherRepositorySystem, + eventSpyDispatcher, + information, + defaultTypeRegistry, + versionScheme, + Collections.emptyMap()); + + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setLocalRepository(getLocalRepository()); + + // Simulate what BaseParser.populateSystemProperties() does — reads real env vars + Properties systemProps = new Properties(); + EnvironmentUtils.addEnvVars(systemProps); + request.setSystemProperties(systemProps); + + RepositorySystemSession session = systemSessionFactory.newRepositorySession(request); + + RemoteRepository httpsRepo = + new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build(); + org.eclipse.aether.repository.Proxy proxy = session.getProxySelector().getProxy(httpsRepo); + assertNotNull(proxy, "Expected proxy to be set from HTTPS_PROXY env var"); + } + protected ArtifactRepository getLocalRepository() throws InvalidRepositoryException { File repoDir = new File(getBasedir(), "target/local-repo").getAbsoluteFile();